Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • /lib/ -> moodlelib.php (source)

    Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       1  <?php
       2  // This file is part of Moodle - http://moodle.org/
       3  //
       4  // Moodle is free software: you can redistribute it and/or modify
       5  // it under the terms of the GNU General Public License as published by
       6  // the Free Software Foundation, either version 3 of the License, or
       7  // (at your option) any later version.
       8  //
       9  // Moodle is distributed in the hope that it will be useful,
      10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
      11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12  // GNU General Public License for more details.
      13  //
      14  // You should have received a copy of the GNU General Public License
      15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
      16  
      17  /**
      18   * moodlelib.php - Moodle main library
      19   *
      20   * Main library file of miscellaneous general-purpose Moodle functions.
      21   * Other main libraries:
      22   *  - weblib.php      - functions that produce web output
      23   *  - datalib.php     - functions that access the database
      24   *
      25   * @package    core
      26   * @subpackage lib
      27   * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
      28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      29   */
      30  
      31  defined('MOODLE_INTERNAL') || die();
      32  
      33  // CONSTANTS (Encased in phpdoc proper comments).
      34  
      35  // Date and time constants.
      36  /**
      37   * Time constant - the number of seconds in a year
      38   */
      39  define('YEARSECS', 31536000);
      40  
      41  /**
      42   * Time constant - the number of seconds in a week
      43   */
      44  define('WEEKSECS', 604800);
      45  
      46  /**
      47   * Time constant - the number of seconds in a day
      48   */
      49  define('DAYSECS', 86400);
      50  
      51  /**
      52   * Time constant - the number of seconds in an hour
      53   */
      54  define('HOURSECS', 3600);
      55  
      56  /**
      57   * Time constant - the number of seconds in a minute
      58   */
      59  define('MINSECS', 60);
      60  
      61  /**
      62   * Time constant - the number of minutes in a day
      63   */
      64  define('DAYMINS', 1440);
      65  
      66  /**
      67   * Time constant - the number of minutes in an hour
      68   */
      69  define('HOURMINS', 60);
      70  
      71  // Parameter constants - every call to optional_param(), required_param()
      72  // or clean_param() should have a specified type of parameter.
      73  
      74  /**
      75   * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
      76   */
      77  define('PARAM_ALPHA',    'alpha');
      78  
      79  /**
      80   * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
      81   * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
      82   */
      83  define('PARAM_ALPHAEXT', 'alphaext');
      84  
      85  /**
      86   * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
      87   */
      88  define('PARAM_ALPHANUM', 'alphanum');
      89  
      90  /**
      91   * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
      92   */
      93  define('PARAM_ALPHANUMEXT', 'alphanumext');
      94  
      95  /**
      96   * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
      97   */
      98  define('PARAM_AUTH',  'auth');
      99  
     100  /**
     101   * PARAM_BASE64 - Base 64 encoded format
     102   */
     103  define('PARAM_BASE64',   'base64');
     104  
     105  /**
     106   * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
     107   */
     108  define('PARAM_BOOL',     'bool');
     109  
     110  /**
     111   * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
     112   * checked against the list of capabilities in the database.
     113   */
     114  define('PARAM_CAPABILITY',   'capability');
     115  
     116  /**
     117   * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
     118   * to use this. The normal mode of operation is to use PARAM_RAW when receiving
     119   * the input (required/optional_param or formslib) and then sanitise the HTML
     120   * using format_text on output. This is for the rare cases when you want to
     121   * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
     122   */
     123  define('PARAM_CLEANHTML', 'cleanhtml');
     124  
     125  /**
     126   * PARAM_EMAIL - an email address following the RFC
     127   */
     128  define('PARAM_EMAIL',   'email');
     129  
     130  /**
     131   * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
     132   */
     133  define('PARAM_FILE',   'file');
     134  
     135  /**
     136   * PARAM_FLOAT - a real/floating point number.
     137   *
     138   * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
     139   * It does not work for languages that use , as a decimal separator.
     140   * Use PARAM_LOCALISEDFLOAT instead.
     141   */
     142  define('PARAM_FLOAT',  'float');
     143  
     144  /**
     145   * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
     146   * This is preferred over PARAM_FLOAT for numbers typed in by the user.
     147   * Cleans localised numbers to computer readable numbers; false for invalid numbers.
     148   */
     149  define('PARAM_LOCALISEDFLOAT',  'localisedfloat');
     150  
     151  /**
     152   * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
     153   */
     154  define('PARAM_HOST',     'host');
     155  
     156  /**
     157   * PARAM_INT - integers only, use when expecting only numbers.
     158   */
     159  define('PARAM_INT',      'int');
     160  
     161  /**
     162   * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
     163   */
     164  define('PARAM_LANG',  'lang');
     165  
     166  /**
     167   * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
     168   * others! Implies PARAM_URL!)
     169   */
     170  define('PARAM_LOCALURL', 'localurl');
     171  
     172  /**
     173   * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
     174   */
     175  define('PARAM_NOTAGS',   'notags');
     176  
     177  /**
     178   * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
     179   * traversals note: the leading slash is not removed, window drive letter is not allowed
     180   */
     181  define('PARAM_PATH',     'path');
     182  
     183  /**
     184   * PARAM_PEM - Privacy Enhanced Mail format
     185   */
     186  define('PARAM_PEM',      'pem');
     187  
     188  /**
     189   * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
     190   */
     191  define('PARAM_PERMISSION',   'permission');
     192  
     193  /**
     194   * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
     195   */
     196  define('PARAM_RAW', 'raw');
     197  
     198  /**
     199   * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
     200   */
     201  define('PARAM_RAW_TRIMMED', 'raw_trimmed');
     202  
     203  /**
     204   * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
     205   */
     206  define('PARAM_SAFEDIR',  'safedir');
     207  
     208  /**
     209   * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
     210   */
     211  define('PARAM_SAFEPATH',  'safepath');
     212  
     213  /**
     214   * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
     215   */
     216  define('PARAM_SEQUENCE',  'sequence');
     217  
     218  /**
     219   * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
     220   */
     221  define('PARAM_TAG',   'tag');
     222  
     223  /**
     224   * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
     225   */
     226  define('PARAM_TAGLIST',   'taglist');
     227  
     228  /**
     229   * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
     230   */
     231  define('PARAM_TEXT',  'text');
     232  
     233  /**
     234   * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
     235   */
     236  define('PARAM_THEME',  'theme');
     237  
     238  /**
     239   * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
     240   * http://localhost.localdomain/ is ok.
     241   */
     242  define('PARAM_URL',      'url');
     243  
     244  /**
     245   * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
     246   * accounts, do NOT use when syncing with external systems!!
     247   */
     248  define('PARAM_USERNAME',    'username');
     249  
     250  /**
     251   * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
     252   */
     253  define('PARAM_STRINGID',    'stringid');
     254  
     255  // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
     256  /**
     257   * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
     258   * It was one of the first types, that is why it is abused so much ;-)
     259   * @deprecated since 2.0
     260   */
     261  define('PARAM_CLEAN',    'clean');
     262  
     263  /**
     264   * PARAM_INTEGER - deprecated alias for PARAM_INT
     265   * @deprecated since 2.0
     266   */
     267  define('PARAM_INTEGER',  'int');
     268  
     269  /**
     270   * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
     271   * @deprecated since 2.0
     272   */
     273  define('PARAM_NUMBER',  'float');
     274  
     275  /**
     276   * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
     277   * NOTE: originally alias for PARAM_APLHA
     278   * @deprecated since 2.0
     279   */
     280  define('PARAM_ACTION',   'alphanumext');
     281  
     282  /**
     283   * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
     284   * NOTE: originally alias for PARAM_APLHA
     285   * @deprecated since 2.0
     286   */
     287  define('PARAM_FORMAT',   'alphanumext');
     288  
     289  /**
     290   * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
     291   * @deprecated since 2.0
     292   */
     293  define('PARAM_MULTILANG',  'text');
     294  
     295  /**
     296   * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
     297   * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
     298   * America/Port-au-Prince)
     299   */
     300  define('PARAM_TIMEZONE', 'timezone');
     301  
     302  /**
     303   * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
     304   */
     305  define('PARAM_CLEANFILE', 'file');
     306  
     307  /**
     308   * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
     309   * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
     310   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
     311   * NOTE: numbers and underscores are strongly discouraged in plugin names!
     312   */
     313  define('PARAM_COMPONENT', 'component');
     314  
     315  /**
     316   * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
     317   * It is usually used together with context id and component.
     318   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
     319   */
     320  define('PARAM_AREA', 'area');
     321  
     322  /**
     323   * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
     324   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
     325   * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
     326   */
     327  define('PARAM_PLUGIN', 'plugin');
     328  
     329  
     330  // Web Services.
     331  
     332  /**
     333   * VALUE_REQUIRED - if the parameter is not supplied, there is an error
     334   */
     335  define('VALUE_REQUIRED', 1);
     336  
     337  /**
     338   * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
     339   */
     340  define('VALUE_OPTIONAL', 2);
     341  
     342  /**
     343   * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
     344   */
     345  define('VALUE_DEFAULT', 0);
     346  
     347  /**
     348   * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
     349   */
     350  define('NULL_NOT_ALLOWED', false);
     351  
     352  /**
     353   * NULL_ALLOWED - the parameter can be set to null in the database
     354   */
     355  define('NULL_ALLOWED', true);
     356  
     357  // Page types.
     358  
     359  /**
     360   * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
     361   */
     362  define('PAGE_COURSE_VIEW', 'course-view');
     363  
     364  /** Get remote addr constant */
     365  define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
     366  /** Get remote addr constant */
     367  define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
     368  /**
     369   * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
     370   */
     371  define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
     372  
     373  // Blog access level constant declaration.
     374  define ('BLOG_USER_LEVEL', 1);
     375  define ('BLOG_GROUP_LEVEL', 2);
     376  define ('BLOG_COURSE_LEVEL', 3);
     377  define ('BLOG_SITE_LEVEL', 4);
     378  define ('BLOG_GLOBAL_LEVEL', 5);
     379  
     380  
     381  // Tag constants.
     382  /**
     383   * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
     384   * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
     385   * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
     386   *
     387   * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
     388   */
     389  define('TAG_MAX_LENGTH', 50);
     390  
     391  // Password policy constants.
     392  define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
     393  define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
     394  define ('PASSWORD_DIGITS', '0123456789');
     395  define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
     396  
     397  // Feature constants.
     398  // Used for plugin_supports() to report features that are, or are not, supported by a module.
     399  
     400  /** True if module can provide a grade */
     401  define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
     402  /** True if module supports outcomes */
     403  define('FEATURE_GRADE_OUTCOMES', 'outcomes');
     404  /** True if module supports advanced grading methods */
     405  define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
     406  /** True if module controls the grade visibility over the gradebook */
     407  define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
     408  /** True if module supports plagiarism plugins */
     409  define('FEATURE_PLAGIARISM', 'plagiarism');
     410  
     411  /** True if module has code to track whether somebody viewed it */
     412  define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
     413  /** True if module has custom completion rules */
     414  define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
     415  
     416  /** True if module has no 'view' page (like label) */
     417  define('FEATURE_NO_VIEW_LINK', 'viewlink');
     418  /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
     419  define('FEATURE_IDNUMBER', 'idnumber');
     420  /** True if module supports groups */
     421  define('FEATURE_GROUPS', 'groups');
     422  /** True if module supports groupings */
     423  define('FEATURE_GROUPINGS', 'groupings');
     424  /**
     425   * True if module supports groupmembersonly (which no longer exists)
     426   * @deprecated Since Moodle 2.8
     427   */
     428  define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
     429  
     430  /** Type of module */
     431  define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
     432  /** True if module supports intro editor */
     433  define('FEATURE_MOD_INTRO', 'mod_intro');
     434  /** True if module has default completion */
     435  define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
     436  
     437  define('FEATURE_COMMENT', 'comment');
     438  
     439  define('FEATURE_RATE', 'rate');
     440  /** True if module supports backup/restore of moodle2 format */
     441  define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
     442  
     443  /** True if module can show description on course main page */
     444  define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
     445  
     446  /** True if module uses the question bank */
     447  define('FEATURE_USES_QUESTIONS', 'usesquestions');
     448  
     449  /**
     450   * Maximum filename char size
     451   */
     452  define('MAX_FILENAME_SIZE', 100);
     453  
     454  /** Unspecified module archetype */
     455  define('MOD_ARCHETYPE_OTHER', 0);
     456  /** Resource-like type module */
     457  define('MOD_ARCHETYPE_RESOURCE', 1);
     458  /** Assignment module archetype */
     459  define('MOD_ARCHETYPE_ASSIGNMENT', 2);
     460  /** System (not user-addable) module archetype */
     461  define('MOD_ARCHETYPE_SYSTEM', 3);
     462  
     463  /**
     464   * Security token used for allowing access
     465   * from external application such as web services.
     466   * Scripts do not use any session, performance is relatively
     467   * low because we need to load access info in each request.
     468   * Scripts are executed in parallel.
     469   */
     470  define('EXTERNAL_TOKEN_PERMANENT', 0);
     471  
     472  /**
     473   * Security token used for allowing access
     474   * of embedded applications, the code is executed in the
     475   * active user session. Token is invalidated after user logs out.
     476   * Scripts are executed serially - normal session locking is used.
     477   */
     478  define('EXTERNAL_TOKEN_EMBEDDED', 1);
     479  
     480  /**
     481   * The home page should be the site home
     482   */
     483  define('HOMEPAGE_SITE', 0);
     484  /**
     485   * The home page should be the users my page
     486   */
     487  define('HOMEPAGE_MY', 1);
     488  /**
     489   * The home page can be chosen by the user
     490   */
     491  define('HOMEPAGE_USER', 2);
     492  
     493  /**
     494   * URL of the Moodle sites registration portal.
     495   */
     496  defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
     497  
     498  /**
     499   * URL of the statistic server public key.
     500   */
     501  defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
     502  
     503  /**
     504   * Moodle mobile app service name
     505   */
     506  define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
     507  
     508  /**
     509   * Indicates the user has the capabilities required to ignore activity and course file size restrictions
     510   */
     511  define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
     512  
     513  /**
     514   * Course display settings: display all sections on one page.
     515   */
     516  define('COURSE_DISPLAY_SINGLEPAGE', 0);
     517  /**
     518   * Course display settings: split pages into a page per section.
     519   */
     520  define('COURSE_DISPLAY_MULTIPAGE', 1);
     521  
     522  /**
     523   * Authentication constant: String used in password field when password is not stored.
     524   */
     525  define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
     526  
     527  /**
     528   * Email from header to never include via information.
     529   */
     530  define('EMAIL_VIA_NEVER', 0);
     531  
     532  /**
     533   * Email from header to always include via information.
     534   */
     535  define('EMAIL_VIA_ALWAYS', 1);
     536  
     537  /**
     538   * Email from header to only include via information if the address is no-reply.
     539   */
     540  define('EMAIL_VIA_NO_REPLY_ONLY', 2);
     541  
     542  // PARAMETER HANDLING.
     543  
     544  /**
     545   * Returns a particular value for the named variable, taken from
     546   * POST or GET.  If the parameter doesn't exist then an error is
     547   * thrown because we require this variable.
     548   *
     549   * This function should be used to initialise all required values
     550   * in a script that are based on parameters.  Usually it will be
     551   * used like this:
     552   *    $id = required_param('id', PARAM_INT);
     553   *
     554   * Please note the $type parameter is now required and the value can not be array.
     555   *
     556   * @param string $parname the name of the page parameter we want
     557   * @param string $type expected type of parameter
     558   * @return mixed
     559   * @throws coding_exception
     560   */
     561  function required_param($parname, $type) {
     562      if (func_num_args() != 2 or empty($parname) or empty($type)) {
     563          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
     564      }
     565      // POST has precedence.
     566      if (isset($_POST[$parname])) {
     567          $param = $_POST[$parname];
     568      } else if (isset($_GET[$parname])) {
     569          $param = $_GET[$parname];
     570      } else {
     571          print_error('missingparam', '', '', $parname);
     572      }
     573  
     574      if (is_array($param)) {
     575          debugging('Invalid array parameter detected in required_param(): '.$parname);
     576          // TODO: switch to fatal error in Moodle 2.3.
     577          return required_param_array($parname, $type);
     578      }
     579  
     580      return clean_param($param, $type);
     581  }
     582  
     583  /**
     584   * Returns a particular array value for the named variable, taken from
     585   * POST or GET.  If the parameter doesn't exist then an error is
     586   * thrown because we require this variable.
     587   *
     588   * This function should be used to initialise all required values
     589   * in a script that are based on parameters.  Usually it will be
     590   * used like this:
     591   *    $ids = required_param_array('ids', PARAM_INT);
     592   *
     593   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
     594   *
     595   * @param string $parname the name of the page parameter we want
     596   * @param string $type expected type of parameter
     597   * @return array
     598   * @throws coding_exception
     599   */
     600  function required_param_array($parname, $type) {
     601      if (func_num_args() != 2 or empty($parname) or empty($type)) {
     602          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
     603      }
     604      // POST has precedence.
     605      if (isset($_POST[$parname])) {
     606          $param = $_POST[$parname];
     607      } else if (isset($_GET[$parname])) {
     608          $param = $_GET[$parname];
     609      } else {
     610          print_error('missingparam', '', '', $parname);
     611      }
     612      if (!is_array($param)) {
     613          print_error('missingparam', '', '', $parname);
     614      }
     615  
     616      $result = array();
     617      foreach ($param as $key => $value) {
     618          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
     619              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
     620              continue;
     621          }
     622          $result[$key] = clean_param($value, $type);
     623      }
     624  
     625      return $result;
     626  }
     627  
     628  /**
     629   * Returns a particular value for the named variable, taken from
     630   * POST or GET, otherwise returning a given default.
     631   *
     632   * This function should be used to initialise all optional values
     633   * in a script that are based on parameters.  Usually it will be
     634   * used like this:
     635   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
     636   *
     637   * Please note the $type parameter is now required and the value can not be array.
     638   *
     639   * @param string $parname the name of the page parameter we want
     640   * @param mixed  $default the default value to return if nothing is found
     641   * @param string $type expected type of parameter
     642   * @return mixed
     643   * @throws coding_exception
     644   */
     645  function optional_param($parname, $default, $type) {
     646      if (func_num_args() != 3 or empty($parname) or empty($type)) {
     647          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
     648      }
     649  
     650      // POST has precedence.
     651      if (isset($_POST[$parname])) {
     652          $param = $_POST[$parname];
     653      } else if (isset($_GET[$parname])) {
     654          $param = $_GET[$parname];
     655      } else {
     656          return $default;
     657      }
     658  
     659      if (is_array($param)) {
     660          debugging('Invalid array parameter detected in required_param(): '.$parname);
     661          // TODO: switch to $default in Moodle 2.3.
     662          return optional_param_array($parname, $default, $type);
     663      }
     664  
     665      return clean_param($param, $type);
     666  }
     667  
     668  /**
     669   * Returns a particular array value for the named variable, taken from
     670   * POST or GET, otherwise returning a given default.
     671   *
     672   * This function should be used to initialise all optional values
     673   * in a script that are based on parameters.  Usually it will be
     674   * used like this:
     675   *    $ids = optional_param('id', array(), PARAM_INT);
     676   *
     677   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
     678   *
     679   * @param string $parname the name of the page parameter we want
     680   * @param mixed $default the default value to return if nothing is found
     681   * @param string $type expected type of parameter
     682   * @return array
     683   * @throws coding_exception
     684   */
     685  function optional_param_array($parname, $default, $type) {
     686      if (func_num_args() != 3 or empty($parname) or empty($type)) {
     687          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
     688      }
     689  
     690      // POST has precedence.
     691      if (isset($_POST[$parname])) {
     692          $param = $_POST[$parname];
     693      } else if (isset($_GET[$parname])) {
     694          $param = $_GET[$parname];
     695      } else {
     696          return $default;
     697      }
     698      if (!is_array($param)) {
     699          debugging('optional_param_array() expects array parameters only: '.$parname);
     700          return $default;
     701      }
     702  
     703      $result = array();
     704      foreach ($param as $key => $value) {
     705          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
     706              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
     707              continue;
     708          }
     709          $result[$key] = clean_param($value, $type);
     710      }
     711  
     712      return $result;
     713  }
     714  
     715  /**
     716   * Strict validation of parameter values, the values are only converted
     717   * to requested PHP type. Internally it is using clean_param, the values
     718   * before and after cleaning must be equal - otherwise
     719   * an invalid_parameter_exception is thrown.
     720   * Objects and classes are not accepted.
     721   *
     722   * @param mixed $param
     723   * @param string $type PARAM_ constant
     724   * @param bool $allownull are nulls valid value?
     725   * @param string $debuginfo optional debug information
     726   * @return mixed the $param value converted to PHP type
     727   * @throws invalid_parameter_exception if $param is not of given type
     728   */
     729  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
     730      if (is_null($param)) {
     731          if ($allownull == NULL_ALLOWED) {
     732              return null;
     733          } else {
     734              throw new invalid_parameter_exception($debuginfo);
     735          }
     736      }
     737      if (is_array($param) or is_object($param)) {
     738          throw new invalid_parameter_exception($debuginfo);
     739      }
     740  
     741      $cleaned = clean_param($param, $type);
     742  
     743      if ($type == PARAM_FLOAT) {
     744          // Do not detect precision loss here.
     745          if (is_float($param) or is_int($param)) {
     746              // These always fit.
     747          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
     748              throw new invalid_parameter_exception($debuginfo);
     749          }
     750      } else if ((string)$param !== (string)$cleaned) {
     751          // Conversion to string is usually lossless.
     752          throw new invalid_parameter_exception($debuginfo);
     753      }
     754  
     755      return $cleaned;
     756  }
     757  
     758  /**
     759   * Makes sure array contains only the allowed types, this function does not validate array key names!
     760   *
     761   * <code>
     762   * $options = clean_param($options, PARAM_INT);
     763   * </code>
     764   *
     765   * @param array $param the variable array we are cleaning
     766   * @param string $type expected format of param after cleaning.
     767   * @param bool $recursive clean recursive arrays
     768   * @return array
     769   * @throws coding_exception
     770   */
     771  function clean_param_array(array $param = null, $type, $recursive = false) {
     772      // Convert null to empty array.
     773      $param = (array)$param;
     774      foreach ($param as $key => $value) {
     775          if (is_array($value)) {
     776              if ($recursive) {
     777                  $param[$key] = clean_param_array($value, $type, true);
     778              } else {
     779                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
     780              }
     781          } else {
     782              $param[$key] = clean_param($value, $type);
     783          }
     784      }
     785      return $param;
     786  }
     787  
     788  /**
     789   * Used by {@link optional_param()} and {@link required_param()} to
     790   * clean the variables and/or cast to specific types, based on
     791   * an options field.
     792   * <code>
     793   * $course->format = clean_param($course->format, PARAM_ALPHA);
     794   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
     795   * </code>
     796   *
     797   * @param mixed $param the variable we are cleaning
     798   * @param string $type expected format of param after cleaning.
     799   * @return mixed
     800   * @throws coding_exception
     801   */
     802  function clean_param($param, $type) {
     803      global $CFG;
     804  
     805      if (is_array($param)) {
     806          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
     807      } else if (is_object($param)) {
     808          if (method_exists($param, '__toString')) {
     809              $param = $param->__toString();
     810          } else {
     811              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
     812          }
     813      }
     814  
     815      switch ($type) {
     816          case PARAM_RAW:
     817              // No cleaning at all.
     818              $param = fix_utf8($param);
     819              return $param;
     820  
     821          case PARAM_RAW_TRIMMED:
     822              // No cleaning, but strip leading and trailing whitespace.
     823              $param = fix_utf8($param);
     824              return trim($param);
     825  
     826          case PARAM_CLEAN:
     827              // General HTML cleaning, try to use more specific type if possible this is deprecated!
     828              // Please use more specific type instead.
     829              if (is_numeric($param)) {
     830                  return $param;
     831              }
     832              $param = fix_utf8($param);
     833              // Sweep for scripts, etc.
     834              return clean_text($param);
     835  
     836          case PARAM_CLEANHTML:
     837              // Clean html fragment.
     838              $param = fix_utf8($param);
     839              // Sweep for scripts, etc.
     840              $param = clean_text($param, FORMAT_HTML);
     841              return trim($param);
     842  
     843          case PARAM_INT:
     844              // Convert to integer.
     845              return (int)$param;
     846  
     847          case PARAM_FLOAT:
     848              // Convert to float.
     849              return (float)$param;
     850  
     851          case PARAM_LOCALISEDFLOAT:
     852              // Convert to float.
     853              return unformat_float($param, true);
     854  
     855          case PARAM_ALPHA:
     856              // Remove everything not `a-z`.
     857              return preg_replace('/[^a-zA-Z]/i', '', $param);
     858  
     859          case PARAM_ALPHAEXT:
     860              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
     861              return preg_replace('/[^a-zA-Z_-]/i', '', $param);
     862  
     863          case PARAM_ALPHANUM:
     864              // Remove everything not `a-zA-Z0-9`.
     865              return preg_replace('/[^A-Za-z0-9]/i', '', $param);
     866  
     867          case PARAM_ALPHANUMEXT:
     868              // Remove everything not `a-zA-Z0-9_-`.
     869              return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
     870  
     871          case PARAM_SEQUENCE:
     872              // Remove everything not `0-9,`.
     873              return preg_replace('/[^0-9,]/i', '', $param);
     874  
     875          case PARAM_BOOL:
     876              // Convert to 1 or 0.
     877              $tempstr = strtolower($param);
     878              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
     879                  $param = 1;
     880              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
     881                  $param = 0;
     882              } else {
     883                  $param = empty($param) ? 0 : 1;
     884              }
     885              return $param;
     886  
     887          case PARAM_NOTAGS:
     888              // Strip all tags.
     889              $param = fix_utf8($param);
     890              return strip_tags($param);
     891  
     892          case PARAM_TEXT:
     893              // Leave only tags needed for multilang.
     894              $param = fix_utf8($param);
     895              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
     896              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
     897              do {
     898                  if (strpos($param, '</lang>') !== false) {
     899                      // Old and future mutilang syntax.
     900                      $param = strip_tags($param, '<lang>');
     901                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
     902                          break;
     903                      }
     904                      $open = false;
     905                      foreach ($matches[0] as $match) {
     906                          if ($match === '</lang>') {
     907                              if ($open) {
     908                                  $open = false;
     909                                  continue;
     910                              } else {
     911                                  break 2;
     912                              }
     913                          }
     914                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
     915                              break 2;
     916                          } else {
     917                              $open = true;
     918                          }
     919                      }
     920                      if ($open) {
     921                          break;
     922                      }
     923                      return $param;
     924  
     925                  } else if (strpos($param, '</span>') !== false) {
     926                      // Current problematic multilang syntax.
     927                      $param = strip_tags($param, '<span>');
     928                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
     929                          break;
     930                      }
     931                      $open = false;
     932                      foreach ($matches[0] as $match) {
     933                          if ($match === '</span>') {
     934                              if ($open) {
     935                                  $open = false;
     936                                  continue;
     937                              } else {
     938                                  break 2;
     939                              }
     940                          }
     941                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
     942                              break 2;
     943                          } else {
     944                              $open = true;
     945                          }
     946                      }
     947                      if ($open) {
     948                          break;
     949                      }
     950                      return $param;
     951                  }
     952              } while (false);
     953              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
     954              return strip_tags($param);
     955  
     956          case PARAM_COMPONENT:
     957              // We do not want any guessing here, either the name is correct or not
     958              // please note only normalised component names are accepted.
     959              if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
     960                  return '';
     961              }
     962              if (strpos($param, '__') !== false) {
     963                  return '';
     964              }
     965              if (strpos($param, 'mod_') === 0) {
     966                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
     967                  if (substr_count($param, '_') != 1) {
     968                      return '';
     969                  }
     970              }
     971              return $param;
     972  
     973          case PARAM_PLUGIN:
     974          case PARAM_AREA:
     975              // We do not want any guessing here, either the name is correct or not.
     976              if (!is_valid_plugin_name($param)) {
     977                  return '';
     978              }
     979              return $param;
     980  
     981          case PARAM_SAFEDIR:
     982              // Remove everything not a-zA-Z0-9_- .
     983              return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
     984  
     985          case PARAM_SAFEPATH:
     986              // Remove everything not a-zA-Z0-9/_- .
     987              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
     988  
     989          case PARAM_FILE:
     990              // Strip all suspicious characters from filename.
     991              $param = fix_utf8($param);
     992              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
     993              if ($param === '.' || $param === '..') {
     994                  $param = '';
     995              }
     996              return $param;
     997  
     998          case PARAM_PATH:
     999              // Strip all suspicious characters from file path.
    1000              $param = fix_utf8($param);
    1001              $param = str_replace('\\', '/', $param);
    1002  
    1003              // Explode the path and clean each element using the PARAM_FILE rules.
    1004              $breadcrumb = explode('/', $param);
    1005              foreach ($breadcrumb as $key => $crumb) {
    1006                  if ($crumb === '.' && $key === 0) {
    1007                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
    1008                  } else {
    1009                      $crumb = clean_param($crumb, PARAM_FILE);
    1010                  }
    1011                  $breadcrumb[$key] = $crumb;
    1012              }
    1013              $param = implode('/', $breadcrumb);
    1014  
    1015              // Remove multiple current path (./././) and multiple slashes (///).
    1016              $param = preg_replace('~//+~', '/', $param);
    1017              $param = preg_replace('~/(\./)+~', '/', $param);
    1018              return $param;
    1019  
    1020          case PARAM_HOST:
    1021              // Allow FQDN or IPv4 dotted quad.
    1022              $param = preg_replace('/[^\.\d\w-]/', '', $param );
    1023              // Match ipv4 dotted quad.
    1024              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
    1025                  // Confirm values are ok.
    1026                  if ( $match[0] > 255
    1027                       || $match[1] > 255
    1028                       || $match[3] > 255
    1029                       || $match[4] > 255 ) {
    1030                      // Hmmm, what kind of dotted quad is this?
    1031                      $param = '';
    1032                  }
    1033              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
    1034                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
    1035                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
    1036                         ) {
    1037                  // All is ok - $param is respected.
    1038              } else {
    1039                  // All is not ok...
    1040                  $param='';
    1041              }
    1042              return $param;
    1043  
    1044          case PARAM_URL:
    1045              // Allow safe urls.
    1046              $param = fix_utf8($param);
    1047              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
    1048              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
    1049                  // All is ok, param is respected.
    1050              } else {
    1051                  // Not really ok.
    1052                  $param ='';
    1053              }
    1054              return $param;
    1055  
    1056          case PARAM_LOCALURL:
    1057              // Allow http absolute, root relative and relative URLs within wwwroot.
    1058              $param = clean_param($param, PARAM_URL);
    1059              if (!empty($param)) {
    1060  
    1061                  if ($param === $CFG->wwwroot) {
    1062                      // Exact match;
    1063                  } else if (preg_match(':^/:', $param)) {
    1064                      // Root-relative, ok!
    1065                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
    1066                      // Absolute, and matches our wwwroot.
    1067                  } else {
    1068                      // Relative - let's make sure there are no tricks.
    1069                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
    1070                          // Looks ok.
    1071                      } else {
    1072                          $param = '';
    1073                      }
    1074                  }
    1075              }
    1076              return $param;
    1077  
    1078          case PARAM_PEM:
    1079              $param = trim($param);
    1080              // PEM formatted strings may contain letters/numbers and the symbols:
    1081              //   forward slash: /
    1082              //   plus sign:     +
    1083              //   equal sign:    =
    1084              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
    1085              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
    1086                  list($wholething, $body) = $matches;
    1087                  unset($wholething, $matches);
    1088                  $b64 = clean_param($body, PARAM_BASE64);
    1089                  if (!empty($b64)) {
    1090                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
    1091                  } else {
    1092                      return '';
    1093                  }
    1094              }
    1095              return '';
    1096  
    1097          case PARAM_BASE64:
    1098              if (!empty($param)) {
    1099                  // PEM formatted strings may contain letters/numbers and the symbols
    1100                  //   forward slash: /
    1101                  //   plus sign:     +
    1102                  //   equal sign:    =.
    1103                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
    1104                      return '';
    1105                  }
    1106                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
    1107                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
    1108                  // than (or equal to) 64 characters long.
    1109                  for ($i=0, $j=count($lines); $i < $j; $i++) {
    1110                      if ($i + 1 == $j) {
    1111                          if (64 < strlen($lines[$i])) {
    1112                              return '';
    1113                          }
    1114                          continue;
    1115                      }
    1116  
    1117                      if (64 != strlen($lines[$i])) {
    1118                          return '';
    1119                      }
    1120                  }
    1121                  return implode("\n", $lines);
    1122              } else {
    1123                  return '';
    1124              }
    1125  
    1126          case PARAM_TAG:
    1127              $param = fix_utf8($param);
    1128              // Please note it is not safe to use the tag name directly anywhere,
    1129              // it must be processed with s(), urlencode() before embedding anywhere.
    1130              // Remove some nasties.
    1131              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
    1132              // Convert many whitespace chars into one.
    1133              $param = preg_replace('/\s+/u', ' ', $param);
    1134              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
    1135              return $param;
    1136  
    1137          case PARAM_TAGLIST:
    1138              $param = fix_utf8($param);
    1139              $tags = explode(',', $param);
    1140              $result = array();
    1141              foreach ($tags as $tag) {
    1142                  $res = clean_param($tag, PARAM_TAG);
    1143                  if ($res !== '') {
    1144                      $result[] = $res;
    1145                  }
    1146              }
    1147              if ($result) {
    1148                  return implode(',', $result);
    1149              } else {
    1150                  return '';
    1151              }
    1152  
    1153          case PARAM_CAPABILITY:
    1154              if (get_capability_info($param)) {
    1155                  return $param;
    1156              } else {
    1157                  return '';
    1158              }
    1159  
    1160          case PARAM_PERMISSION:
    1161              $param = (int)$param;
    1162              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
    1163                  return $param;
    1164              } else {
    1165                  return CAP_INHERIT;
    1166              }
    1167  
    1168          case PARAM_AUTH:
    1169              $param = clean_param($param, PARAM_PLUGIN);
    1170              if (empty($param)) {
    1171                  return '';
    1172              } else if (exists_auth_plugin($param)) {
    1173                  return $param;
    1174              } else {
    1175                  return '';
    1176              }
    1177  
    1178          case PARAM_LANG:
    1179              $param = clean_param($param, PARAM_SAFEDIR);
    1180              if (get_string_manager()->translation_exists($param)) {
    1181                  return $param;
    1182              } else {
    1183                  // Specified language is not installed or param malformed.
    1184                  return '';
    1185              }
    1186  
    1187          case PARAM_THEME:
    1188              $param = clean_param($param, PARAM_PLUGIN);
    1189              if (empty($param)) {
    1190                  return '';
    1191              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
    1192                  return $param;
    1193              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
    1194                  return $param;
    1195              } else {
    1196                  // Specified theme is not installed.
    1197                  return '';
    1198              }
    1199  
    1200          case PARAM_USERNAME:
    1201              $param = fix_utf8($param);
    1202              $param = trim($param);
    1203              // Convert uppercase to lowercase MDL-16919.
    1204              $param = core_text::strtolower($param);
    1205              if (empty($CFG->extendedusernamechars)) {
    1206                  $param = str_replace(" " , "", $param);
    1207                  // Regular expression, eliminate all chars EXCEPT:
    1208                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
    1209                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
    1210              }
    1211              return $param;
    1212  
    1213          case PARAM_EMAIL:
    1214              $param = fix_utf8($param);
    1215              if (validate_email($param)) {
    1216                  return $param;
    1217              } else {
    1218                  return '';
    1219              }
    1220  
    1221          case PARAM_STRINGID:
    1222              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
    1223                  return $param;
    1224              } else {
    1225                  return '';
    1226              }
    1227  
    1228          case PARAM_TIMEZONE:
    1229              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
    1230              $param = fix_utf8($param);
    1231              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
    1232              if (preg_match($timezonepattern, $param)) {
    1233                  return $param;
    1234              } else {
    1235                  return '';
    1236              }
    1237  
    1238          default:
    1239              // Doh! throw error, switched parameters in optional_param or another serious problem.
    1240              print_error("unknownparamtype", '', '', $type);
    1241      }
    1242  }
    1243  
    1244  /**
    1245   * Whether the PARAM_* type is compatible in RTL.
    1246   *
    1247   * Being compatible with RTL means that the data they contain can flow
    1248   * from right-to-left or left-to-right without compromising the user experience.
    1249   *
    1250   * Take URLs for example, they are not RTL compatible as they should always
    1251   * flow from the left to the right. This also applies to numbers, email addresses,
    1252   * configuration snippets, base64 strings, etc...
    1253   *
    1254   * This function tries to best guess which parameters can contain localised strings.
    1255   *
    1256   * @param string $paramtype Constant PARAM_*.
    1257   * @return bool
    1258   */
    1259  function is_rtl_compatible($paramtype) {
    1260      return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
    1261  }
    1262  
    1263  /**
    1264   * Makes sure the data is using valid utf8, invalid characters are discarded.
    1265   *
    1266   * Note: this function is not intended for full objects with methods and private properties.
    1267   *
    1268   * @param mixed $value
    1269   * @return mixed with proper utf-8 encoding
    1270   */
    1271  function fix_utf8($value) {
    1272      if (is_null($value) or $value === '') {
    1273          return $value;
    1274  
    1275      } else if (is_string($value)) {
    1276          if ((string)(int)$value === $value) {
    1277              // Shortcut.
    1278              return $value;
    1279          }
    1280          // No null bytes expected in our data, so let's remove it.
    1281          $value = str_replace("\0", '', $value);
    1282  
    1283          // Note: this duplicates min_fix_utf8() intentionally.
    1284          static $buggyiconv = null;
    1285          if ($buggyiconv === null) {
    1286              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
    1287          }
    1288  
    1289          if ($buggyiconv) {
    1290              if (function_exists('mb_convert_encoding')) {
    1291                  $subst = mb_substitute_character();
    1292                  mb_substitute_character('none');
    1293                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
    1294                  mb_substitute_character($subst);
    1295  
    1296              } else {
    1297                  // Warn admins on admin/index.php page.
    1298                  $result = $value;
    1299              }
    1300  
    1301          } else {
    1302              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
    1303          }
    1304  
    1305          return $result;
    1306  
    1307      } else if (is_array($value)) {
    1308          foreach ($value as $k => $v) {
    1309              $value[$k] = fix_utf8($v);
    1310          }
    1311          return $value;
    1312  
    1313      } else if (is_object($value)) {
    1314          // Do not modify original.
    1315          $value = clone($value);
    1316          foreach ($value as $k => $v) {
    1317              $value->$k = fix_utf8($v);
    1318          }
    1319          return $value;
    1320  
    1321      } else {
    1322          // This is some other type, no utf-8 here.
    1323          return $value;
    1324      }
    1325  }
    1326  
    1327  /**
    1328   * Return true if given value is integer or string with integer value
    1329   *
    1330   * @param mixed $value String or Int
    1331   * @return bool true if number, false if not
    1332   */
    1333  function is_number($value) {
    1334      if (is_int($value)) {
    1335          return true;
    1336      } else if (is_string($value)) {
    1337          return ((string)(int)$value) === $value;
    1338      } else {
    1339          return false;
    1340      }
    1341  }
    1342  
    1343  /**
    1344   * Returns host part from url.
    1345   *
    1346   * @param string $url full url
    1347   * @return string host, null if not found
    1348   */
    1349  function get_host_from_url($url) {
    1350      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
    1351      if ($matches) {
    1352          return $matches[1];
    1353      }
    1354      return null;
    1355  }
    1356  
    1357  /**
    1358   * Tests whether anything was returned by text editor
    1359   *
    1360   * This function is useful for testing whether something you got back from
    1361   * the HTML editor actually contains anything. Sometimes the HTML editor
    1362   * appear to be empty, but actually you get back a <br> tag or something.
    1363   *
    1364   * @param string $string a string containing HTML.
    1365   * @return boolean does the string contain any actual content - that is text,
    1366   * images, objects, etc.
    1367   */
    1368  function html_is_blank($string) {
    1369      return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
    1370  }
    1371  
    1372  /**
    1373   * Set a key in global configuration
    1374   *
    1375   * Set a key/value pair in both this session's {@link $CFG} global variable
    1376   * and in the 'config' database table for future sessions.
    1377   *
    1378   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
    1379   * In that case it doesn't affect $CFG.
    1380   *
    1381   * A NULL value will delete the entry.
    1382   *
    1383   * NOTE: this function is called from lib/db/upgrade.php
    1384   *
    1385   * @param string $name the key to set
    1386   * @param string $value the value to set (without magic quotes)
    1387   * @param string $plugin (optional) the plugin scope, default null
    1388   * @return bool true or exception
    1389   */
    1390  function set_config($name, $value, $plugin=null) {
    1391      global $CFG, $DB;
    1392  
    1393      if (empty($plugin)) {
    1394          if (!array_key_exists($name, $CFG->config_php_settings)) {
    1395              // So it's defined for this invocation at least.
    1396              if (is_null($value)) {
    1397                  unset($CFG->$name);
    1398              } else {
    1399                  // Settings from db are always strings.
    1400                  $CFG->$name = (string)$value;
    1401              }
    1402          }
    1403  
    1404          if ($DB->get_field('config', 'name', array('name' => $name))) {
    1405              if ($value === null) {
    1406                  $DB->delete_records('config', array('name' => $name));
    1407              } else {
    1408                  $DB->set_field('config', 'value', $value, array('name' => $name));
    1409              }
    1410          } else {
    1411              if ($value !== null) {
    1412                  $config = new stdClass();
    1413                  $config->name  = $name;
    1414                  $config->value = $value;
    1415                  $DB->insert_record('config', $config, false);
    1416              }
    1417              // When setting config during a Behat test (in the CLI script, not in the web browser
    1418              // requests), remember which ones are set so that we can clear them later.
    1419              if (defined('BEHAT_TEST')) {
    1420                  if (!property_exists($CFG, 'behat_cli_added_config')) {
    1421                      $CFG->behat_cli_added_config = [];
    1422                  }
    1423                  $CFG->behat_cli_added_config[$name] = true;
    1424              }
    1425          }
    1426          if ($name === 'siteidentifier') {
    1427              cache_helper::update_site_identifier($value);
    1428          }
    1429          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
    1430      } else {
    1431          // Plugin scope.
    1432          if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
    1433              if ($value===null) {
    1434                  $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
    1435              } else {
    1436                  $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
    1437              }
    1438          } else {
    1439              if ($value !== null) {
    1440                  $config = new stdClass();
    1441                  $config->plugin = $plugin;
    1442                  $config->name   = $name;
    1443                  $config->value  = $value;
    1444                  $DB->insert_record('config_plugins', $config, false);
    1445              }
    1446          }
    1447          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
    1448      }
    1449  
    1450      return true;
    1451  }
    1452  
    1453  /**
    1454   * Get configuration values from the global config table
    1455   * or the config_plugins table.
    1456   *
    1457   * If called with one parameter, it will load all the config
    1458   * variables for one plugin, and return them as an object.
    1459   *
    1460   * If called with 2 parameters it will return a string single
    1461   * value or false if the value is not found.
    1462   *
    1463   * NOTE: this function is called from lib/db/upgrade.php
    1464   *
    1465   * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
    1466   *     that we need only fetch it once per request.
    1467   * @param string $plugin full component name
    1468   * @param string $name default null
    1469   * @return mixed hash-like object or single value, return false no config found
    1470   * @throws dml_exception
    1471   */
    1472  function get_config($plugin, $name = null) {
    1473      global $CFG, $DB;
    1474  
    1475      static $siteidentifier = null;
    1476  
    1477      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
    1478          $forced =& $CFG->config_php_settings;
    1479          $iscore = true;
    1480          $plugin = 'core';
    1481      } else {
    1482          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
    1483              $forced =& $CFG->forced_plugin_settings[$plugin];
    1484          } else {
    1485              $forced = array();
    1486          }
    1487          $iscore = false;
    1488      }
    1489  
    1490      if ($siteidentifier === null) {
    1491          try {
    1492              // This may fail during installation.
    1493              // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
    1494              // install the database.
    1495              $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
    1496          } catch (dml_exception $ex) {
    1497              // Set siteidentifier to false. We don't want to trip this continually.
    1498              $siteidentifier = false;
    1499              throw $ex;
    1500          }
    1501      }
    1502  
    1503      if (!empty($name)) {
    1504          if (array_key_exists($name, $forced)) {
    1505              return (string)$forced[$name];
    1506          } else if ($name === 'siteidentifier' && $plugin == 'core') {
    1507              return $siteidentifier;
    1508          }
    1509      }
    1510  
    1511      $cache = cache::make('core', 'config');
    1512      $result = $cache->get($plugin);
    1513      if ($result === false) {
    1514          // The user is after a recordset.
    1515          if (!$iscore) {
    1516              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
    1517          } else {
    1518              // This part is not really used any more, but anyway...
    1519              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
    1520          }
    1521          $cache->set($plugin, $result);
    1522      }
    1523  
    1524      if (!empty($name)) {
    1525          if (array_key_exists($name, $result)) {
    1526              return $result[$name];
    1527          }
    1528          return false;
    1529      }
    1530  
    1531      if ($plugin === 'core') {
    1532          $result['siteidentifier'] = $siteidentifier;
    1533      }
    1534  
    1535      foreach ($forced as $key => $value) {
    1536          if (is_null($value) or is_array($value) or is_object($value)) {
    1537              // We do not want any extra mess here, just real settings that could be saved in db.
    1538              unset($result[$key]);
    1539          } else {
    1540              // Convert to string as if it went through the DB.
    1541              $result[$key] = (string)$value;
    1542          }
    1543      }
    1544  
    1545      return (object)$result;
    1546  }
    1547  
    1548  /**
    1549   * Removes a key from global configuration.
    1550   *
    1551   * NOTE: this function is called from lib/db/upgrade.php
    1552   *
    1553   * @param string $name the key to set
    1554   * @param string $plugin (optional) the plugin scope
    1555   * @return boolean whether the operation succeeded.
    1556   */
    1557  function unset_config($name, $plugin=null) {
    1558      global $CFG, $DB;
    1559  
    1560      if (empty($plugin)) {
    1561          unset($CFG->$name);
    1562          $DB->delete_records('config', array('name' => $name));
    1563          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
    1564      } else {
    1565          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
    1566          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
    1567      }
    1568  
    1569      return true;
    1570  }
    1571  
    1572  /**
    1573   * Remove all the config variables for a given plugin.
    1574   *
    1575   * NOTE: this function is called from lib/db/upgrade.php
    1576   *
    1577   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
    1578   * @return boolean whether the operation succeeded.
    1579   */
    1580  function unset_all_config_for_plugin($plugin) {
    1581      global $DB;
    1582      // Delete from the obvious config_plugins first.
    1583      $DB->delete_records('config_plugins', array('plugin' => $plugin));
    1584      // Next delete any suspect settings from config.
    1585      $like = $DB->sql_like('name', '?', true, true, false, '|');
    1586      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
    1587      $DB->delete_records_select('config', $like, $params);
    1588      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
    1589      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
    1590  
    1591      return true;
    1592  }
    1593  
    1594  /**
    1595   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
    1596   *
    1597   * All users are verified if they still have the necessary capability.
    1598   *
    1599   * @param string $value the value of the config setting.
    1600   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
    1601   * @param bool $includeadmins include administrators.
    1602   * @return array of user objects.
    1603   */
    1604  function get_users_from_config($value, $capability, $includeadmins = true) {
    1605      if (empty($value) or $value === '$@NONE@$') {
    1606          return array();
    1607      }
    1608  
    1609      // We have to make sure that users still have the necessary capability,
    1610      // it should be faster to fetch them all first and then test if they are present
    1611      // instead of validating them one-by-one.
    1612      $users = get_users_by_capability(context_system::instance(), $capability);
    1613      if ($includeadmins) {
    1614          $admins = get_admins();
    1615          foreach ($admins as $admin) {
    1616              $users[$admin->id] = $admin;
    1617          }
    1618      }
    1619  
    1620      if ($value === '$@ALL@$') {
    1621          return $users;
    1622      }
    1623  
    1624      $result = array(); // Result in correct order.
    1625      $allowed = explode(',', $value);
    1626      foreach ($allowed as $uid) {
    1627          if (isset($users[$uid])) {
    1628              $user = $users[$uid];
    1629              $result[$user->id] = $user;
    1630          }
    1631      }
    1632  
    1633      return $result;
    1634  }
    1635  
    1636  
    1637  /**
    1638   * Invalidates browser caches and cached data in temp.
    1639   *
    1640   * @return void
    1641   */
    1642  function purge_all_caches() {
    1643      purge_caches();
    1644  }
    1645  
    1646  /**
    1647   * Selectively invalidate different types of cache.
    1648   *
    1649   * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
    1650   * areas alone or in combination.
    1651   *
    1652   * @param bool[] $options Specific parts of the cache to purge. Valid options are:
    1653   *        'muc'    Purge MUC caches?
    1654   *        'theme'  Purge theme cache?
    1655   *        'lang'   Purge language string cache?
    1656   *        'js'     Purge javascript cache?
    1657   *        'filter' Purge text filter cache?
    1658   *        'other'  Purge all other caches?
    1659   */
    1660  function purge_caches($options = []) {
    1661      $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
    1662      if (empty(array_filter($options))) {
    1663          $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
    1664      } else {
    1665          $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
    1666      }
    1667      if ($options['muc']) {
    1668          cache_helper::purge_all();
    1669      }
    1670      if ($options['theme']) {
    1671          theme_reset_all_caches();
    1672      }
    1673      if ($options['lang']) {
    1674          get_string_manager()->reset_caches();
    1675      }
    1676      if ($options['js']) {
    1677          js_reset_all_caches();
    1678      }
    1679      if ($options['template']) {
    1680          template_reset_all_caches();
    1681      }
    1682      if ($options['filter']) {
    1683          reset_text_filters_cache();
    1684      }
    1685      if ($options['other']) {
    1686          purge_other_caches();
    1687      }
    1688  }
    1689  
    1690  /**
    1691   * Purge all non-MUC caches not otherwise purged in purge_caches.
    1692   *
    1693   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
    1694   * {@link phpunit_util::reset_dataroot()}
    1695   */
    1696  function purge_other_caches() {
    1697      global $DB, $CFG;
    1698      core_text::reset_caches();
    1699      if (class_exists('core_plugin_manager')) {
    1700          core_plugin_manager::reset_caches();
    1701      }
    1702  
    1703      // Bump up cacherev field for all courses.
    1704      try {
    1705          increment_revision_number('course', 'cacherev', '');
    1706      } catch (moodle_exception $e) {
    1707          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
    1708      }
    1709  
    1710      $DB->reset_caches();
    1711  
    1712      // Purge all other caches: rss, simplepie, etc.
    1713      clearstatcache();
    1714      remove_dir($CFG->cachedir.'', true);
    1715  
    1716      // Make sure cache dir is writable, throws exception if not.
    1717      make_cache_directory('');
    1718  
    1719      // This is the only place where we purge local caches, we are only adding files there.
    1720      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
    1721      remove_dir($CFG->localcachedir, true);
    1722      set_config('localcachedirpurged', time());
    1723      make_localcache_directory('', true);
    1724      \core\task\manager::clear_static_caches();
    1725  }
    1726  
    1727  /**
    1728   * Get volatile flags
    1729   *
    1730   * @param string $type
    1731   * @param int $changedsince default null
    1732   * @return array records array
    1733   */
    1734  function get_cache_flags($type, $changedsince = null) {
    1735      global $DB;
    1736  
    1737      $params = array('type' => $type, 'expiry' => time());
    1738      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
    1739      if ($changedsince !== null) {
    1740          $params['changedsince'] = $changedsince;
    1741          $sqlwhere .= " AND timemodified > :changedsince";
    1742      }
    1743      $cf = array();
    1744      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
    1745          foreach ($flags as $flag) {
    1746              $cf[$flag->name] = $flag->value;
    1747          }
    1748      }
    1749      return $cf;
    1750  }
    1751  
    1752  /**
    1753   * Get volatile flags
    1754   *
    1755   * @param string $type
    1756   * @param string $name
    1757   * @param int $changedsince default null
    1758   * @return string|false The cache flag value or false
    1759   */
    1760  function get_cache_flag($type, $name, $changedsince=null) {
    1761      global $DB;
    1762  
    1763      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
    1764  
    1765      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
    1766      if ($changedsince !== null) {
    1767          $params['changedsince'] = $changedsince;
    1768          $sqlwhere .= " AND timemodified > :changedsince";
    1769      }
    1770  
    1771      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
    1772  }
    1773  
    1774  /**
    1775   * Set a volatile flag
    1776   *
    1777   * @param string $type the "type" namespace for the key
    1778   * @param string $name the key to set
    1779   * @param string $value the value to set (without magic quotes) - null will remove the flag
    1780   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
    1781   * @return bool Always returns true
    1782   */
    1783  function set_cache_flag($type, $name, $value, $expiry = null) {
    1784      global $DB;
    1785  
    1786      $timemodified = time();
    1787      if ($expiry === null || $expiry < $timemodified) {
    1788          $expiry = $timemodified + 24 * 60 * 60;
    1789      } else {
    1790          $expiry = (int)$expiry;
    1791      }
    1792  
    1793      if ($value === null) {
    1794          unset_cache_flag($type, $name);
    1795          return true;
    1796      }
    1797  
    1798      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
    1799          // This is a potential problem in DEBUG_DEVELOPER.
    1800          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
    1801              return true; // No need to update.
    1802          }
    1803          $f->value        = $value;
    1804          $f->expiry       = $expiry;
    1805          $f->timemodified = $timemodified;
    1806          $DB->update_record('cache_flags', $f);
    1807      } else {
    1808          $f = new stdClass();
    1809          $f->flagtype     = $type;
    1810          $f->name         = $name;
    1811          $f->value        = $value;
    1812          $f->expiry       = $expiry;
    1813          $f->timemodified = $timemodified;
    1814          $DB->insert_record('cache_flags', $f);
    1815      }
    1816      return true;
    1817  }
    1818  
    1819  /**
    1820   * Removes a single volatile flag
    1821   *
    1822   * @param string $type the "type" namespace for the key
    1823   * @param string $name the key to set
    1824   * @return bool
    1825   */
    1826  function unset_cache_flag($type, $name) {
    1827      global $DB;
    1828      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
    1829      return true;
    1830  }
    1831  
    1832  /**
    1833   * Garbage-collect volatile flags
    1834   *
    1835   * @return bool Always returns true
    1836   */
    1837  function gc_cache_flags() {
    1838      global $DB;
    1839      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
    1840      return true;
    1841  }
    1842  
    1843  // USER PREFERENCE API.
    1844  
    1845  /**
    1846   * Refresh user preference cache. This is used most often for $USER
    1847   * object that is stored in session, but it also helps with performance in cron script.
    1848   *
    1849   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
    1850   *
    1851   * @package  core
    1852   * @category preference
    1853   * @access   public
    1854   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
    1855   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
    1856   * @throws   coding_exception
    1857   * @return   null
    1858   */
    1859  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
    1860      global $DB;
    1861      // Static cache, we need to check on each page load, not only every 2 minutes.
    1862      static $loadedusers = array();
    1863  
    1864      if (!isset($user->id)) {
    1865          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
    1866      }
    1867  
    1868      if (empty($user->id) or isguestuser($user->id)) {
    1869          // No permanent storage for not-logged-in users and guest.
    1870          if (!isset($user->preference)) {
    1871              $user->preference = array();
    1872          }
    1873          return;
    1874      }
    1875  
    1876      $timenow = time();
    1877  
    1878      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
    1879          // Already loaded at least once on this page. Are we up to date?
    1880          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
    1881              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
    1882              return;
    1883  
    1884          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
    1885              // No change since the lastcheck on this page.
    1886              $user->preference['_lastloaded'] = $timenow;
    1887              return;
    1888          }
    1889      }
    1890  
    1891      // OK, so we have to reload all preferences.
    1892      $loadedusers[$user->id] = true;
    1893      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
    1894      $user->preference['_lastloaded'] = $timenow;
    1895  }
    1896  
    1897  /**
    1898   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
    1899   *
    1900   * NOTE: internal function, do not call from other code.
    1901   *
    1902   * @package core
    1903   * @access private
    1904   * @param integer $userid the user whose prefs were changed.
    1905   */
    1906  function mark_user_preferences_changed($userid) {
    1907      global $CFG;
    1908  
    1909      if (empty($userid) or isguestuser($userid)) {
    1910          // No cache flags for guest and not-logged-in users.
    1911          return;
    1912      }
    1913  
    1914      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
    1915  }
    1916  
    1917  /**
    1918   * Sets a preference for the specified user.
    1919   *
    1920   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
    1921   *
    1922   * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
    1923   *
    1924   * @package  core
    1925   * @category preference
    1926   * @access   public
    1927   * @param    string            $name  The key to set as preference for the specified user
    1928   * @param    string            $value The value to set for the $name key in the specified user's
    1929   *                                    record, null means delete current value.
    1930   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
    1931   * @throws   coding_exception
    1932   * @return   bool                     Always true or exception
    1933   */
    1934  function set_user_preference($name, $value, $user = null) {
    1935      global $USER, $DB;
    1936  
    1937      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
    1938          throw new coding_exception('Invalid preference name in set_user_preference() call');
    1939      }
    1940  
    1941      if (is_null($value)) {
    1942          // Null means delete current.
    1943          return unset_user_preference($name, $user);
    1944      } else if (is_object($value)) {
    1945          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
    1946      } else if (is_array($value)) {
    1947          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
    1948      }
    1949      // Value column maximum length is 1333 characters.
    1950      $value = (string)$value;
    1951      if (core_text::strlen($value) > 1333) {
    1952          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
    1953      }
    1954  
    1955      if (is_null($user)) {
    1956          $user = $USER;
    1957      } else if (isset($user->id)) {
    1958          // It is a valid object.
    1959      } else if (is_numeric($user)) {
    1960          $user = (object)array('id' => (int)$user);
    1961      } else {
    1962          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
    1963      }
    1964  
    1965      check_user_preferences_loaded($user);
    1966  
    1967      if (empty($user->id) or isguestuser($user->id)) {
    1968          // No permanent storage for not-logged-in users and guest.
    1969          $user->preference[$name] = $value;
    1970          return true;
    1971      }
    1972  
    1973      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
    1974          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
    1975              // Preference already set to this value.
    1976              return true;
    1977          }
    1978          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
    1979  
    1980      } else {
    1981          $preference = new stdClass();
    1982          $preference->userid = $user->id;
    1983          $preference->name   = $name;
    1984          $preference->value  = $value;
    1985          $DB->insert_record('user_preferences', $preference);
    1986      }
    1987  
    1988      // Update value in cache.
    1989      $user->preference[$name] = $value;
    1990      // Update the $USER in case where we've not a direct reference to $USER.
    1991      if ($user !== $USER && $user->id == $USER->id) {
    1992          $USER->preference[$name] = $value;
    1993      }
    1994  
    1995      // Set reload flag for other sessions.
    1996      mark_user_preferences_changed($user->id);
    1997  
    1998      return true;
    1999  }
    2000  
    2001  /**
    2002   * Sets a whole array of preferences for the current user
    2003   *
    2004   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
    2005   *
    2006   * @package  core
    2007   * @category preference
    2008   * @access   public
    2009   * @param    array             $prefarray An array of key/value pairs to be set
    2010   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
    2011   * @return   bool                         Always true or exception
    2012   */
    2013  function set_user_preferences(array $prefarray, $user = null) {
    2014      foreach ($prefarray as $name => $value) {
    2015          set_user_preference($name, $value, $user);
    2016      }
    2017      return true;
    2018  }
    2019  
    2020  /**
    2021   * Unsets a preference completely by deleting it from the database
    2022   *
    2023   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
    2024   *
    2025   * @package  core
    2026   * @category preference
    2027   * @access   public
    2028   * @param    string            $name The key to unset as preference for the specified user
    2029   * @param    stdClass|int|null $user A moodle user object or id, null means current user
    2030   * @throws   coding_exception
    2031   * @return   bool                    Always true or exception
    2032   */
    2033  function unset_user_preference($name, $user = null) {
    2034      global $USER, $DB;
    2035  
    2036      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
    2037          throw new coding_exception('Invalid preference name in unset_user_preference() call');
    2038      }
    2039  
    2040      if (is_null($user)) {
    2041          $user = $USER;
    2042      } else if (isset($user->id)) {
    2043          // It is a valid object.
    2044      } else if (is_numeric($user)) {
    2045          $user = (object)array('id' => (int)$user);
    2046      } else {
    2047          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
    2048      }
    2049  
    2050      check_user_preferences_loaded($user);
    2051  
    2052      if (empty($user->id) or isguestuser($user->id)) {
    2053          // No permanent storage for not-logged-in user and guest.
    2054          unset($user->preference[$name]);
    2055          return true;
    2056      }
    2057  
    2058      // Delete from DB.
    2059      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
    2060  
    2061      // Delete the preference from cache.
    2062      unset($user->preference[$name]);
    2063      // Update the $USER in case where we've not a direct reference to $USER.
    2064      if ($user !== $USER && $user->id == $USER->id) {
    2065          unset($USER->preference[$name]);
    2066      }
    2067  
    2068      // Set reload flag for other sessions.
    2069      mark_user_preferences_changed($user->id);
    2070  
    2071      return true;
    2072  }
    2073  
    2074  /**
    2075   * Used to fetch user preference(s)
    2076   *
    2077   * If no arguments are supplied this function will return
    2078   * all of the current user preferences as an array.
    2079   *
    2080   * If a name is specified then this function
    2081   * attempts to return that particular preference value.  If
    2082   * none is found, then the optional value $default is returned,
    2083   * otherwise null.
    2084   *
    2085   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
    2086   *
    2087   * @package  core
    2088   * @category preference
    2089   * @access   public
    2090   * @param    string            $name    Name of the key to use in finding a preference value
    2091   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
    2092   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
    2093   * @throws   coding_exception
    2094   * @return   string|mixed|null          A string containing the value of a single preference. An
    2095   *                                      array with all of the preferences or null
    2096   */
    2097  function get_user_preferences($name = null, $default = null, $user = null) {
    2098      global $USER;
    2099  
    2100      if (is_null($name)) {
    2101          // All prefs.
    2102      } else if (is_numeric($name) or $name === '_lastloaded') {
    2103          throw new coding_exception('Invalid preference name in get_user_preferences() call');
    2104      }
    2105  
    2106      if (is_null($user)) {
    2107          $user = $USER;
    2108      } else if (isset($user->id)) {
    2109          // Is a valid object.
    2110      } else if (is_numeric($user)) {
    2111          if ($USER->id == $user) {
    2112              $user = $USER;
    2113          } else {
    2114              $user = (object)array('id' => (int)$user);
    2115          }
    2116      } else {
    2117          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
    2118      }
    2119  
    2120      check_user_preferences_loaded($user);
    2121  
    2122      if (empty($name)) {
    2123          // All values.
    2124          return $user->preference;
    2125      } else if (isset($user->preference[$name])) {
    2126          // The single string value.
    2127          return $user->preference[$name];
    2128      } else {
    2129          // Default value (null if not specified).
    2130          return $default;
    2131      }
    2132  }
    2133  
    2134  // FUNCTIONS FOR HANDLING TIME.
    2135  
    2136  /**
    2137   * Given Gregorian date parts in user time produce a GMT timestamp.
    2138   *
    2139   * @package core
    2140   * @category time
    2141   * @param int $year The year part to create timestamp of
    2142   * @param int $month The month part to create timestamp of
    2143   * @param int $day The day part to create timestamp of
    2144   * @param int $hour The hour part to create timestamp of
    2145   * @param int $minute The minute part to create timestamp of
    2146   * @param int $second The second part to create timestamp of
    2147   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
    2148   *             if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
    2149   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
    2150   *             applied only if timezone is 99 or string.
    2151   * @return int GMT timestamp
    2152   */
    2153  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
    2154      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
    2155      $date->setDate((int)$year, (int)$month, (int)$day);
    2156      $date->setTime((int)$hour, (int)$minute, (int)$second);
    2157  
    2158      $time = $date->getTimestamp();
    2159  
    2160      if ($time === false) {
    2161          throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
    2162              ' This can fail if year is more than 2038 and OS is 32 bit windows');
    2163      }
    2164  
    2165      // Moodle BC DST stuff.
    2166      if (!$applydst) {
    2167          $time += dst_offset_on($time, $timezone);
    2168      }
    2169  
    2170      return $time;
    2171  
    2172  }
    2173  
    2174  /**
    2175   * Format a date/time (seconds) as weeks, days, hours etc as needed
    2176   *
    2177   * Given an amount of time in seconds, returns string
    2178   * formatted nicely as years, days, hours etc as needed
    2179   *
    2180   * @package core
    2181   * @category time
    2182   * @uses MINSECS
    2183   * @uses HOURSECS
    2184   * @uses DAYSECS
    2185   * @uses YEARSECS
    2186   * @param int $totalsecs Time in seconds
    2187   * @param stdClass $str Should be a time object
    2188   * @return string A nicely formatted date/time string
    2189   */
    2190  function format_time($totalsecs, $str = null) {
    2191  
    2192      $totalsecs = abs($totalsecs);
    2193  
    2194      if (!$str) {
    2195          // Create the str structure the slow way.
    2196          $str = new stdClass();
    2197          $str->day   = get_string('day');
    2198          $str->days  = get_string('days');
    2199          $str->hour  = get_string('hour');
    2200          $str->hours = get_string('hours');
    2201          $str->min   = get_string('min');
    2202          $str->mins  = get_string('mins');
    2203          $str->sec   = get_string('sec');
    2204          $str->secs  = get_string('secs');
    2205          $str->year  = get_string('year');
    2206          $str->years = get_string('years');
    2207      }
    2208  
    2209      $years     = floor($totalsecs/YEARSECS);
    2210      $remainder = $totalsecs - ($years*YEARSECS);
    2211      $days      = floor($remainder/DAYSECS);
    2212      $remainder = $totalsecs - ($days*DAYSECS);
    2213      $hours     = floor($remainder/HOURSECS);
    2214      $remainder = $remainder - ($hours*HOURSECS);
    2215      $mins      = floor($remainder/MINSECS);
    2216      $secs      = $remainder - ($mins*MINSECS);
    2217  
    2218      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
    2219      $sm = ($mins == 1)  ? $str->min  : $str->mins;
    2220      $sh = ($hours == 1) ? $str->hour : $str->hours;
    2221      $sd = ($days == 1)  ? $str->day  : $str->days;
    2222      $sy = ($years == 1)  ? $str->year  : $str->years;
    2223  
    2224      $oyears = '';
    2225      $odays = '';
    2226      $ohours = '';
    2227      $omins = '';
    2228      $osecs = '';
    2229  
    2230      if ($years) {
    2231          $oyears  = $years .' '. $sy;
    2232      }
    2233      if ($days) {
    2234          $odays  = $days .' '. $sd;
    2235      }
    2236      if ($hours) {
    2237          $ohours = $hours .' '. $sh;
    2238      }
    2239      if ($mins) {
    2240          $omins  = $mins .' '. $sm;
    2241      }
    2242      if ($secs) {
    2243          $osecs  = $secs .' '. $ss;
    2244      }
    2245  
    2246      if ($years) {
    2247          return trim($oyears .' '. $odays);
    2248      }
    2249      if ($days) {
    2250          return trim($odays .' '. $ohours);
    2251      }
    2252      if ($hours) {
    2253          return trim($ohours .' '. $omins);
    2254      }
    2255      if ($mins) {
    2256          return trim($omins .' '. $osecs);
    2257      }
    2258      if ($secs) {
    2259          return $osecs;
    2260      }
    2261      return get_string('now');
    2262  }
    2263  
    2264  /**
    2265   * Returns a formatted string that represents a date in user time.
    2266   *
    2267   * @package core
    2268   * @category time
    2269   * @param int $date the timestamp in UTC, as obtained from the database.
    2270   * @param string $format strftime format. You should probably get this using
    2271   *        get_string('strftime...', 'langconfig');
    2272   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
    2273   *        not 99 then daylight saving will not be added.
    2274   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
    2275   * @param bool $fixday If true (default) then the leading zero from %d is removed.
    2276   *        If false then the leading zero is maintained.
    2277   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
    2278   * @return string the formatted date/time.
    2279   */
    2280  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
    2281      $calendartype = \core_calendar\type_factory::get_calendar_instance();
    2282      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
    2283  }
    2284  
    2285  /**
    2286   * Returns a html "time" tag with both the exact user date with timezone information
    2287   * as a datetime attribute in the W3C format, and the user readable date and time as text.
    2288   *
    2289   * @package core
    2290   * @category time
    2291   * @param int $date the timestamp in UTC, as obtained from the database.
    2292   * @param string $format strftime format. You should probably get this using
    2293   *        get_string('strftime...', 'langconfig');
    2294   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
    2295   *        not 99 then daylight saving will not be added.
    2296   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
    2297   * @param bool $fixday If true (default) then the leading zero from %d is removed.
    2298   *        If false then the leading zero is maintained.
    2299   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
    2300   * @return string the formatted date/time.
    2301   */
    2302  function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
    2303      $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
    2304      if (CLI_SCRIPT && !PHPUNIT_TEST) {
    2305          return $userdatestr;
    2306      }
    2307      $machinedate = new DateTime();
    2308      $machinedate->setTimestamp(intval($date));
    2309      $machinedate->setTimezone(core_date::get_user_timezone_object());
    2310  
    2311      return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
    2312  }
    2313  
    2314  /**
    2315   * Returns a formatted date ensuring it is UTF-8.
    2316   *
    2317   * If we are running under Windows convert to Windows encoding and then back to UTF-8
    2318   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
    2319   *
    2320   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
    2321   * @param string $format strftime format.
    2322   * @param int|float|string $tz the user timezone
    2323   * @return string the formatted date/time.
    2324   * @since Moodle 2.3.3
    2325   */
    2326  function date_format_string($date, $format, $tz = 99) {
    2327      global $CFG;
    2328  
    2329      $localewincharset = null;
    2330      // Get the calendar type user is using.
    2331      if ($CFG->ostype == 'WINDOWS') {
    2332          $calendartype = \core_calendar\type_factory::get_calendar_instance();
    2333          $localewincharset = $calendartype->locale_win_charset();
    2334      }
    2335  
    2336      if ($localewincharset) {
    2337          $format = core_text::convert($format, 'utf-8', $localewincharset);
    2338      }
    2339  
    2340      date_default_timezone_set(core_date::get_user_timezone($tz));
    2341      $datestring = strftime($format, $date);
    2342      core_date::set_default_server_timezone();
    2343  
    2344      if ($localewincharset) {
    2345          $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
    2346      }
    2347  
    2348      return $datestring;
    2349  }
    2350  
    2351  /**
    2352   * Given a $time timestamp in GMT (seconds since epoch),
    2353   * returns an array that represents the Gregorian date in user time
    2354   *
    2355   * @package core
    2356   * @category time
    2357   * @param int $time Timestamp in GMT
    2358   * @param float|int|string $timezone user timezone
    2359   * @return array An array that represents the date in user time
    2360   */
    2361  function usergetdate($time, $timezone=99) {
    2362      if ($time === null) {
    2363          // PHP8 and PHP7 return different results when getdate(null) is called.
    2364          // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
    2365          // In the future versions of Moodle we may consider adding a strict typehint.
    2366          debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
    2367          $time = 0;
    2368      }
    2369  
    2370      date_default_timezone_set(core_date::get_user_timezone($timezone));
    2371      $result = getdate($time);
    2372      core_date::set_default_server_timezone();
    2373  
    2374      return $result;
    2375  }
    2376  
    2377  /**
    2378   * Given a GMT timestamp (seconds since epoch), offsets it by
    2379   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
    2380   *
    2381   * NOTE: this function does not include DST properly,
    2382   *       you should use the PHP date stuff instead!
    2383   *
    2384   * @package core
    2385   * @category time
    2386   * @param int $date Timestamp in GMT
    2387   * @param float|int|string $timezone user timezone
    2388   * @return int
    2389   */
    2390  function usertime($date, $timezone=99) {
    2391      $userdate = new DateTime('@' . $date);
    2392      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
    2393      $dst = dst_offset_on($date, $timezone);
    2394  
    2395      return $date - $userdate->getOffset() + $dst;
    2396  }
    2397  
    2398  /**
    2399   * Get a formatted string representation of an interval between two unix timestamps.
    2400   *
    2401   * E.g.
    2402   * $intervalstring = get_time_interval_string(12345600, 12345660);
    2403   * Will produce the string:
    2404   * '0d 0h 1m'
    2405   *
    2406   * @param int $time1 unix timestamp
    2407   * @param int $time2 unix timestamp
    2408   * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
    2409   * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
    2410   */
    2411  function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
    2412      $dtdate = new DateTime();
    2413      $dtdate->setTimeStamp($time1);
    2414      $dtdate2 = new DateTime();
    2415      $dtdate2->setTimeStamp($time2);
    2416      $interval = $dtdate2->diff($dtdate);
    2417      $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
    2418      return $interval->format($format);
    2419  }
    2420  
    2421  /**
    2422   * Given a time, return the GMT timestamp of the most recent midnight
    2423   * for the current user.
    2424   *
    2425   * @package core
    2426   * @category time
    2427   * @param int $date Timestamp in GMT
    2428   * @param float|int|string $timezone user timezone
    2429   * @return int Returns a GMT timestamp
    2430   */
    2431  function usergetmidnight($date, $timezone=99) {
    2432  
    2433      $userdate = usergetdate($date, $timezone);
    2434  
    2435      // Time of midnight of this user's day, in GMT.
    2436      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
    2437  
    2438  }
    2439  
    2440  /**
    2441   * Returns a string that prints the user's timezone
    2442   *
    2443   * @package core
    2444   * @category time
    2445   * @param float|int|string $timezone user timezone
    2446   * @return string
    2447   */
    2448  function usertimezone($timezone=99) {
    2449      $tz = core_date::get_user_timezone($timezone);
    2450      return core_date::get_localised_timezone($tz);
    2451  }
    2452  
    2453  /**
    2454   * Returns a float or a string which denotes the user's timezone
    2455   * A float value means that a simple offset from GMT is used, while a string (it will be the name of a timezone in the database)
    2456   * means that for this timezone there are also DST rules to be taken into account
    2457   * Checks various settings and picks the most dominant of those which have a value
    2458   *
    2459   * @package core
    2460   * @category time
    2461   * @param float|int|string $tz timezone to calculate GMT time offset before
    2462   *        calculating user timezone, 99 is default user timezone
    2463   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
    2464   * @return float|string
    2465   */
    2466  function get_user_timezone($tz = 99) {
    2467      global $USER, $CFG;
    2468  
    2469      $timezones = array(
    2470          $tz,
    2471          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
    2472          isset($USER->timezone) ? $USER->timezone : 99,
    2473          isset($CFG->timezone) ? $CFG->timezone : 99,
    2474          );
    2475  
    2476      $tz = 99;
    2477  
    2478      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
    2479      foreach ($timezones as $nextvalue) {
    2480          if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
    2481              $tz = $nextvalue;
    2482          }
    2483      }
    2484      return is_numeric($tz) ? (float) $tz : $tz;
    2485  }
    2486  
    2487  /**
    2488   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
    2489   * - Note: Daylight saving only works for string timezones and not for float.
    2490   *
    2491   * @package core
    2492   * @category time
    2493   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
    2494   * @param int|float|string $strtimezone user timezone
    2495   * @return int
    2496   */
    2497  function dst_offset_on($time, $strtimezone = null) {
    2498      $tz = core_date::get_user_timezone($strtimezone);
    2499      $date = new DateTime('@' . $time);
    2500      $date->setTimezone(new DateTimeZone($tz));
    2501      if ($date->format('I') == '1') {
    2502          if ($tz === 'Australia/Lord_Howe') {
    2503              return 1800;
    2504          }
    2505          return 3600;
    2506      }
    2507      return 0;
    2508  }
    2509  
    2510  /**
    2511   * Calculates when the day appears in specific month
    2512   *
    2513   * @package core
    2514   * @category time
    2515   * @param int $startday starting day of the month
    2516   * @param int $weekday The day when week starts (normally taken from user preferences)
    2517   * @param int $month The month whose day is sought
    2518   * @param int $year The year of the month whose day is sought
    2519   * @return int
    2520   */
    2521  function find_day_in_month($startday, $weekday, $month, $year) {
    2522      $calendartype = \core_calendar\type_factory::get_calendar_instance();
    2523  
    2524      $daysinmonth = days_in_month($month, $year);
    2525      $daysinweek = count($calendartype->get_weekdays());
    2526  
    2527      if ($weekday == -1) {
    2528          // Don't care about weekday, so return:
    2529          //    abs($startday) if $startday != -1
    2530          //    $daysinmonth otherwise.
    2531          return ($startday == -1) ? $daysinmonth : abs($startday);
    2532      }
    2533  
    2534      // From now on we 're looking for a specific weekday.
    2535      // Give "end of month" its actual value, since we know it.
    2536      if ($startday == -1) {
    2537          $startday = -1 * $daysinmonth;
    2538      }
    2539  
    2540      // Starting from day $startday, the sign is the direction.
    2541      if ($startday < 1) {
    2542          $startday = abs($startday);
    2543          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
    2544  
    2545          // This is the last such weekday of the month.
    2546          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
    2547          if ($lastinmonth > $daysinmonth) {
    2548              $lastinmonth -= $daysinweek;
    2549          }
    2550  
    2551          // Find the first such weekday <= $startday.
    2552          while ($lastinmonth > $startday) {
    2553              $lastinmonth -= $daysinweek;
    2554          }
    2555  
    2556          return $lastinmonth;
    2557      } else {
    2558          $indexweekday = dayofweek($startday, $month, $year);
    2559  
    2560          $diff = $weekday - $indexweekday;
    2561          if ($diff < 0) {
    2562              $diff += $daysinweek;
    2563          }
    2564  
    2565          // This is the first such weekday of the month equal to or after $startday.
    2566          $firstfromindex = $startday + $diff;
    2567  
    2568          return $firstfromindex;
    2569      }
    2570  }
    2571  
    2572  /**
    2573   * Calculate the number of days in a given month
    2574   *
    2575   * @package core
    2576   * @category time
    2577   * @param int $month The month whose day count is sought
    2578   * @param int $year The year of the month whose day count is sought
    2579   * @return int
    2580   */
    2581  function days_in_month($month, $year) {
    2582      $calendartype = \core_calendar\type_factory::get_calendar_instance();
    2583      return $calendartype->get_num_days_in_month($year, $month);
    2584  }
    2585  
    2586  /**
    2587   * Calculate the position in the week of a specific calendar day
    2588   *
    2589   * @package core
    2590   * @category time
    2591   * @param int $day The day of the date whose position in the week is sought
    2592   * @param int $month The month of the date whose position in the week is sought
    2593   * @param int $year The year of the date whose position in the week is sought
    2594   * @return int
    2595   */
    2596  function dayofweek($day, $month, $year) {
    2597      $calendartype = \core_calendar\type_factory::get_calendar_instance();
    2598      return $calendartype->get_weekday($year, $month, $day);
    2599  }
    2600  
    2601  // USER AUTHENTICATION AND LOGIN.
    2602  
    2603  /**
    2604   * Returns full login url.
    2605   *
    2606   * Any form submissions for authentication to this URL must include username,
    2607   * password as well as a logintoken generated by \core\session\manager::get_login_token().
    2608   *
    2609   * @return string login url
    2610   */
    2611  function get_login_url() {
    2612      global $CFG;
    2613  
    2614      return "$CFG->wwwroot/login/index.php";
    2615  }
    2616  
    2617  /**
    2618   * This function checks that the current user is logged in and has the
    2619   * required privileges
    2620   *
    2621   * This function checks that the current user is logged in, and optionally
    2622   * whether they are allowed to be in a particular course and view a particular
    2623   * course module.
    2624   * If they are not logged in, then it redirects them to the site login unless
    2625   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
    2626   * case they are automatically logged in as guests.
    2627   * If $courseid is given and the user is not enrolled in that course then the
    2628   * user is redirected to the course enrolment page.
    2629   * If $cm is given and the course module is hidden and the user is not a teacher
    2630   * in the course then the user is redirected to the course home page.
    2631   *
    2632   * When $cm parameter specified, this function sets page layout to 'module'.
    2633   * You need to change it manually later if some other layout needed.
    2634   *
    2635   * @package    core_access
    2636   * @category   access
    2637   *
    2638   * @param mixed $courseorid id of the course or course object
    2639   * @param bool $autologinguest default true
    2640   * @param object $cm course module object
    2641   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
    2642   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
    2643   *             in order to keep redirects working properly. MDL-14495
    2644   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
    2645   * @return mixed Void, exit, and die depending on path
    2646   * @throws coding_exception
    2647   * @throws require_login_exception
    2648   * @throws moodle_exception
    2649   */
    2650  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
    2651      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
    2652  
    2653      // Must not redirect when byteserving already started.
    2654      if (!empty($_SERVER['HTTP_RANGE'])) {
    2655          $preventredirect = true;
    2656      }
    2657  
    2658      if (AJAX_SCRIPT) {
    2659          // We cannot redirect for AJAX scripts either.
    2660          $preventredirect = true;
    2661      }
    2662  
    2663      // Setup global $COURSE, themes, language and locale.
    2664      if (!empty($courseorid)) {
    2665          if (is_object($courseorid)) {
    2666              $course = $courseorid;
    2667          } else if ($courseorid == SITEID) {
    2668              $course = clone($SITE);
    2669          } else {
    2670              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
    2671          }
    2672          if ($cm) {
    2673              if ($cm->course != $course->id) {
    2674                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
    2675              }
    2676              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
    2677              if (!($cm instanceof cm_info)) {
    2678                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
    2679                  // db queries so this is not really a performance concern, however it is obviously
    2680                  // better if you use get_fast_modinfo to get the cm before calling this.
    2681                  $modinfo = get_fast_modinfo($course);
    2682                  $cm = $modinfo->get_cm($cm->id);
    2683              }
    2684          }
    2685      } else {
    2686          // Do not touch global $COURSE via $PAGE->set_course(),
    2687          // the reasons is we need to be able to call require_login() at any time!!
    2688          $course = $SITE;
    2689          if ($cm) {
    2690              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
    2691          }
    2692      }
    2693  
    2694      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
    2695      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
    2696      // risk leading the user back to the AJAX request URL.
    2697      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
    2698          $setwantsurltome = false;
    2699      }
    2700  
    2701      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
    2702      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
    2703          if ($preventredirect) {
    2704              throw new require_login_session_timeout_exception();
    2705          } else {
    2706              if ($setwantsurltome) {
    2707                  $SESSION->wantsurl = qualified_me();
    2708              }
    2709              redirect(get_login_url());
    2710          }
    2711      }
    2712  
    2713      // If the user is not even logged in yet then make sure they are.
    2714      if (!isloggedin()) {
    2715          if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
    2716              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
    2717                  // Misconfigured site guest, just redirect to login page.
    2718                  redirect(get_login_url());
    2719                  exit; // Never reached.
    2720              }
    2721              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
    2722              complete_user_login($guest);
    2723              $USER->autologinguest = true;
    2724              $SESSION->lang = $lang;
    2725          } else {
    2726              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
    2727              if ($preventredirect) {
    2728                  throw new require_login_exception('You are not logged in');
    2729              }
    2730  
    2731              if ($setwantsurltome) {
    2732                  $SESSION->wantsurl = qualified_me();
    2733              }
    2734  
    2735              // Give auth plugins an opportunity to authenticate or redirect to an external login page
    2736              $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
    2737              foreach($authsequence as $authname) {
    2738                  $authplugin = get_auth_plugin($authname);
    2739                  $authplugin->pre_loginpage_hook();
    2740                  if (isloggedin()) {
    2741                      if ($cm) {
    2742                          $modinfo = get_fast_modinfo($course);
    2743                          $cm = $modinfo->get_cm($cm->id);
    2744                      }
    2745                      set_access_log_user();
    2746                      break;
    2747                  }
    2748              }
    2749  
    2750              // If we're still not logged in then go to the login page
    2751              if (!isloggedin()) {
    2752                  redirect(get_login_url());
    2753                  exit; // Never reached.
    2754              }
    2755          }
    2756      }
    2757  
    2758      // Loginas as redirection if needed.
    2759      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
    2760          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
    2761              if ($USER->loginascontext->instanceid != $course->id) {
    2762                  print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
    2763              }
    2764          }
    2765      }
    2766  
    2767      // Check whether the user should be changing password (but only if it is REALLY them).
    2768      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
    2769          $userauth = get_auth_plugin($USER->auth);
    2770          if ($userauth->can_change_password() and !$preventredirect) {
    2771              if ($setwantsurltome) {
    2772                  $SESSION->wantsurl = qualified_me();
    2773              }
    2774              if ($changeurl = $userauth->change_password_url()) {
    2775                  // Use plugin custom url.
    2776                  redirect($changeurl);
    2777              } else {
    2778                  // Use moodle internal method.
    2779                  redirect($CFG->wwwroot .'/login/change_password.php');
    2780              }
    2781          } else if ($userauth->can_change_password()) {
    2782              throw new moodle_exception('forcepasswordchangenotice');
    2783          } else {
    2784              throw new moodle_exception('nopasswordchangeforced', 'auth');
    2785          }
    2786      }
    2787  
    2788      // Check that the user account is properly set up. If we can't redirect to
    2789      // edit their profile and this is not a WS request, perform just the lax check.
    2790      // It will allow them to use filepicker on the profile edit page.
    2791  
    2792      if ($preventredirect && !WS_SERVER) {
    2793          $usernotfullysetup = user_not_fully_set_up($USER, false);
    2794      } else {
    2795          $usernotfullysetup = user_not_fully_set_up($USER, true);
    2796      }
    2797  
    2798      if ($usernotfullysetup) {
    2799          if ($preventredirect) {
    2800              throw new moodle_exception('usernotfullysetup');
    2801          }
    2802          if ($setwantsurltome) {
    2803              $SESSION->wantsurl = qualified_me();
    2804          }
    2805          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
    2806      }
    2807  
    2808      // Make sure the USER has a sesskey set up. Used for CSRF protection.
    2809      sesskey();
    2810  
    2811      if (\core\session\manager::is_loggedinas()) {
    2812          // During a "logged in as" session we should force all content to be cleaned because the
    2813          // logged in user will be viewing potentially malicious user generated content.
    2814          // See MDL-63786 for more details.
    2815          $CFG->forceclean = true;
    2816      }
    2817  
    2818      $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
    2819  
    2820      // Do not bother admins with any formalities, except for activities pending deletion.
    2821      if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
    2822          // Set the global $COURSE.
    2823          if ($cm) {
    2824              $PAGE->set_cm($cm, $course);
    2825              $PAGE->set_pagelayout('incourse');
    2826          } else if (!empty($courseorid)) {
    2827              $PAGE->set_course($course);
    2828          }
    2829          // Set accesstime or the user will appear offline which messes up messaging.
    2830          // Do not update access time for webservice or ajax requests.
    2831          if (!WS_SERVER && !AJAX_SCRIPT) {
    2832              user_accesstime_log($course->id);
    2833          }
    2834  
    2835          foreach ($afterlogins as $plugintype => $plugins) {
    2836              foreach ($plugins as $pluginfunction) {
    2837                  $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
    2838              }
    2839          }
    2840          return;
    2841      }
    2842  
    2843      // Scripts have a chance to declare that $USER->policyagreed should not be checked.
    2844      // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
    2845      if (!defined('NO_SITEPOLICY_CHECK')) {
    2846          define('NO_SITEPOLICY_CHECK', false);
    2847      }
    2848  
    2849      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
    2850      // Do not test if the script explicitly asked for skipping the site policies check.
    2851      if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) {
    2852          $manager = new \core_privacy\local\sitepolicy\manager();
    2853          if ($policyurl = $manager->get_redirect_url(isguestuser())) {
    2854              if ($preventredirect) {
    2855                  throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
    2856              }
    2857              if ($setwantsurltome) {
    2858                  $SESSION->wantsurl = qualified_me();
    2859              }
    2860              redirect($policyurl);
    2861          }
    2862      }
    2863  
    2864      // Fetch the system context, the course context, and prefetch its child contexts.
    2865      $sysctx = context_system::instance();
    2866      $coursecontext = context_course::instance($course->id, MUST_EXIST);
    2867      if ($cm) {
    2868          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
    2869      } else {
    2870          $cmcontext = null;
    2871      }
    2872  
    2873      // If the site is currently under maintenance, then print a message.
    2874      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
    2875          if ($preventredirect) {
    2876              throw new require_login_exception('Maintenance in progress');
    2877          }
    2878          $PAGE->set_context(null);
    2879          print_maintenance_message();
    2880      }
    2881  
    2882      // Make sure the course itself is not hidden.
    2883      if ($course->id == SITEID) {
    2884          // Frontpage can not be hidden.
    2885      } else {
    2886          if (is_role_switched($course->id)) {
    2887              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
    2888          } else {
    2889              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
    2890                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
    2891                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
    2892                  if ($preventredirect) {
    2893                      throw new require_login_exception('Course is hidden');
    2894                  }
    2895                  $PAGE->set_context(null);
    2896                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
    2897                  // the navigation will mess up when trying to find it.
    2898                  navigation_node::override_active_url(new moodle_url('/'));
    2899                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
    2900              }
    2901          }
    2902      }
    2903  
    2904      // Is the user enrolled?
    2905      if ($course->id == SITEID) {
    2906          // Everybody is enrolled on the frontpage.
    2907      } else {
    2908          if (\core\session\manager::is_loggedinas()) {
    2909              // Make sure the REAL person can access this course first.
    2910              $realuser = \core\session\manager::get_realuser();
    2911              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
    2912                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
    2913                  if ($preventredirect) {
    2914                      throw new require_login_exception('Invalid course login-as access');
    2915                  }
    2916                  $PAGE->set_context(null);
    2917                  echo $OUTPUT->header();
    2918                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
    2919              }
    2920          }
    2921  
    2922          $access = false;
    2923  
    2924          if (is_role_switched($course->id)) {
    2925              // Ok, user had to be inside this course before the switch.
    2926              $access = true;
    2927  
    2928          } else if (is_viewing($coursecontext, $USER)) {
    2929              // Ok, no need to mess with enrol.
    2930              $access = true;
    2931  
    2932          } else {
    2933              if (isset($USER->enrol['enrolled'][$course->id])) {
    2934                  if ($USER->enrol['enrolled'][$course->id] > time()) {
    2935                      $access = true;
    2936                      if (isset($USER->enrol['tempguest'][$course->id])) {
    2937                          unset($USER->enrol['tempguest'][$course->id]);
    2938                          remove_temp_course_roles($coursecontext);
    2939                      }
    2940                  } else {
    2941                      // Expired.
    2942                      unset($USER->enrol['enrolled'][$course->id]);
    2943                  }
    2944              }
    2945              if (isset($USER->enrol['tempguest'][$course->id])) {
    2946                  if ($USER->enrol['tempguest'][$course->id] == 0) {
    2947                      $access = true;
    2948                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
    2949                      $access = true;
    2950                  } else {
    2951                      // Expired.
    2952                      unset($USER->enrol['tempguest'][$course->id]);
    2953                      remove_temp_course_roles($coursecontext);
    2954                  }
    2955              }
    2956  
    2957              if (!$access) {
    2958                  // Cache not ok.
    2959                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
    2960                  if ($until !== false) {
    2961                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
    2962                      if ($until == 0) {
    2963                          $until = ENROL_MAX_TIMESTAMP;
    2964                      }
    2965                      $USER->enrol['enrolled'][$course->id] = $until;
    2966                      $access = true;
    2967  
    2968                  } else if (core_course_category::can_view_course_info($course)) {
    2969                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
    2970                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
    2971                      $enrols = enrol_get_plugins(true);
    2972                      // First ask all enabled enrol instances in course if they want to auto enrol user.
    2973                      foreach ($instances as $instance) {
    2974                          if (!isset($enrols[$instance->enrol])) {
    2975                              continue;
    2976                          }
    2977                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
    2978                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
    2979                          if ($until !== false) {
    2980                              if ($until == 0) {
    2981                                  $until = ENROL_MAX_TIMESTAMP;
    2982                              }
    2983                              $USER->enrol['enrolled'][$course->id] = $until;
    2984                              $access = true;
    2985                              break;
    2986                          }
    2987                      }
    2988                      // If not enrolled yet try to gain temporary guest access.
    2989                      if (!$access) {
    2990                          foreach ($instances as $instance) {
    2991                              if (!isset($enrols[$instance->enrol])) {
    2992                                  continue;
    2993                              }
    2994                              // Get a duration for the guest access, a timestamp in the future or false.
    2995                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
    2996                              if ($until !== false and $until > time()) {
    2997                                  $USER->enrol['tempguest'][$course->id] = $until;
    2998                                  $access = true;
    2999                                  break;
    3000                              }
    3001                          }
    3002                      }
    3003                  } else {
    3004                      // User is not enrolled and is not allowed to browse courses here.
    3005                      if ($preventredirect) {
    3006                          throw new require_login_exception('Course is not available');
    3007                      }
    3008                      $PAGE->set_context(null);
    3009                      // We need to override the navigation URL as the course won't have been added to the navigation and thus
    3010                      // the navigation will mess up when trying to find it.
    3011                      navigation_node::override_active_url(new moodle_url('/'));
    3012                      notice(get_string('coursehidden'), $CFG->wwwroot .'/');
    3013                  }
    3014              }
    3015          }
    3016  
    3017          if (!$access) {
    3018              if ($preventredirect) {
    3019                  throw new require_login_exception('Not enrolled');
    3020              }
    3021              if ($setwantsurltome) {
    3022                  $SESSION->wantsurl = qualified_me();
    3023              }
    3024              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
    3025          }
    3026      }
    3027  
    3028      // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
    3029      if ($cm && $cm->deletioninprogress) {
    3030          if ($preventredirect) {
    3031              throw new moodle_exception('activityisscheduledfordeletion');
    3032          }
    3033          require_once($CFG->dirroot . '/course/lib.php');
    3034          redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
    3035      }
    3036  
    3037      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
    3038      if ($cm && !$cm->uservisible) {
    3039          if ($preventredirect) {
    3040              throw new require_login_exception('Activity is hidden');
    3041          }
    3042          // Get the error message that activity is not available and why (if explanation can be shown to the user).
    3043          $PAGE->set_course($course);
    3044          $renderer = $PAGE->get_renderer('course');
    3045          $message = $renderer->course_section_cm_unavailable_error_message($cm);
    3046          redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
    3047      }
    3048  
    3049      // Set the global $COURSE.
    3050      if ($cm) {
    3051          $PAGE->set_cm($cm, $course);
    3052          $PAGE->set_pagelayout('incourse');
    3053      } else if (!empty($courseorid)) {
    3054          $PAGE->set_course($course);
    3055      }
    3056  
    3057      foreach ($afterlogins as $plugintype => $plugins) {
    3058          foreach ($plugins as $pluginfunction) {
    3059              $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
    3060          }
    3061      }
    3062  
    3063      // Finally access granted, update lastaccess times.
    3064      // Do not update access time for webservice or ajax requests.
    3065      if (!WS_SERVER && !AJAX_SCRIPT) {
    3066          user_accesstime_log($course->id);
    3067      }
    3068  }
    3069  
    3070  /**
    3071   * A convenience function for where we must be logged in as admin
    3072   * @return void
    3073   */
    3074  function require_admin() {
    3075      require_login(null, false);
    3076      require_capability('moodle/site:config', context_system::instance());
    3077  }
    3078  
    3079  /**
    3080   * This function just makes sure a user is logged out.
    3081   *
    3082   * @package    core_access
    3083   * @category   access
    3084   */
    3085  function require_logout() {
    3086      global $USER, $DB;
    3087  
    3088      if (!isloggedin()) {
    3089          // This should not happen often, no need for hooks or events here.
    3090          \core\session\manager::terminate_current();
    3091          return;
    3092      }
    3093  
    3094      // Execute hooks before action.
    3095      $authplugins = array();
    3096      $authsequence = get_enabled_auth_plugins();
    3097      foreach ($authsequence as $authname) {
    3098          $authplugins[$authname] = get_auth_plugin($authname);
    3099          $authplugins[$authname]->prelogout_hook();
    3100      }
    3101  
    3102      // Store info that gets removed during logout.
    3103      $sid = session_id();
    3104      $event = \core\event\user_loggedout::create(
    3105          array(
    3106              'userid' => $USER->id,
    3107              'objectid' => $USER->id,
    3108              'other' => array('sessionid' => $sid),
    3109          )
    3110      );
    3111      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
    3112          $event->add_record_snapshot('sessions', $session);
    3113      }
    3114  
    3115      // Clone of $USER object to be used by auth plugins.
    3116      $user = fullclone($USER);
    3117  
    3118      // Delete session record and drop $_SESSION content.
    3119      \core\session\manager::terminate_current();
    3120  
    3121      // Trigger event AFTER action.
    3122      $event->trigger();
    3123  
    3124      // Hook to execute auth plugins redirection after event trigger.
    3125      foreach ($authplugins as $authplugin) {
    3126          $authplugin->postlogout_hook($user);
    3127      }
    3128  }
    3129  
    3130  /**
    3131   * Weaker version of require_login()
    3132   *
    3133   * This is a weaker version of {@link require_login()} which only requires login
    3134   * when called from within a course rather than the site page, unless
    3135   * the forcelogin option is turned on.
    3136   * @see require_login()
    3137   *
    3138   * @package    core_access
    3139   * @category   access
    3140   *
    3141   * @param mixed $courseorid The course object or id in question
    3142   * @param bool $autologinguest Allow autologin guests if that is wanted
    3143   * @param object $cm Course activity module if known
    3144   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
    3145   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
    3146   *             in order to keep redirects working properly. MDL-14495
    3147   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
    3148   * @return void
    3149   * @throws coding_exception
    3150   */
    3151  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
    3152      global $CFG, $PAGE, $SITE;
    3153      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
    3154            or (!is_object($courseorid) and $courseorid == SITEID));
    3155      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
    3156          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
    3157          // db queries so this is not really a performance concern, however it is obviously
    3158          // better if you use get_fast_modinfo to get the cm before calling this.
    3159          if (is_object($courseorid)) {
    3160              $course = $courseorid;
    3161          } else {
    3162              $course = clone($SITE);
    3163          }
    3164          $modinfo = get_fast_modinfo($course);
    3165          $cm = $modinfo->get_cm($cm->id);
    3166      }
    3167      if (!empty($CFG->forcelogin)) {
    3168          // Login required for both SITE and courses.
    3169          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
    3170  
    3171      } else if ($issite && !empty($cm) and !$cm->uservisible) {
    3172          // Always login for hidden activities.
    3173          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
    3174  
    3175      } else if (isloggedin() && !isguestuser()) {
    3176          // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
    3177          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
    3178  
    3179      } else if ($issite) {
    3180          // Login for SITE not required.
    3181          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
    3182          if (!empty($courseorid)) {
    3183              if (is_object($courseorid)) {
    3184                  $course = $courseorid;
    3185              } else {
    3186                  $course = clone $SITE;
    3187              }
    3188              if ($cm) {
    3189                  if ($cm->course != $course->id) {
    3190                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
    3191                  }
    3192                  $PAGE->set_cm($cm, $course);
    3193                  $PAGE->set_pagelayout('incourse');
    3194              } else {
    3195                  $PAGE->set_course($course);
    3196              }
    3197          } else {
    3198              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
    3199              $PAGE->set_course($PAGE->course);
    3200          }
    3201          // Do not update access time for webservice or ajax requests.
    3202          if (!WS_SERVER && !AJAX_SCRIPT) {
    3203              user_accesstime_log(SITEID);
    3204          }
    3205          return;
    3206  
    3207      } else {
    3208          // Course login always required.
    3209          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
    3210      }
    3211  }
    3212  
    3213  /**
    3214   * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
    3215   *
    3216   * @param  string $keyvalue the key value
    3217   * @param  string $script   unique script identifier
    3218   * @param  int $instance    instance id
    3219   * @return stdClass the key entry in the user_private_key table
    3220   * @since Moodle 3.2
    3221   * @throws moodle_exception
    3222   */
    3223  function validate_user_key($keyvalue, $script, $instance) {
    3224      global $DB;
    3225  
    3226      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
    3227          print_error('invalidkey');
    3228      }
    3229  
    3230      if (!empty($key->validuntil) and $key->validuntil < time()) {
    3231          print_error('expiredkey');
    3232      }
    3233  
    3234      if ($key->iprestriction) {
    3235          $remoteaddr = getremoteaddr(null);
    3236          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
    3237              print_error('ipmismatch');
    3238          }
    3239      }
    3240      return $key;
    3241  }
    3242  
    3243  /**
    3244   * Require key login. Function terminates with error if key not found or incorrect.
    3245   *
    3246   * @uses NO_MOODLE_COOKIES
    3247   * @uses PARAM_ALPHANUM
    3248   * @param string $script unique script identifier
    3249   * @param int $instance optional instance id
    3250   * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
    3251   * @return int Instance ID
    3252   */
    3253  function require_user_key_login($script, $instance = null, $keyvalue = null) {
    3254      global $DB;
    3255  
    3256      if (!NO_MOODLE_COOKIES) {
    3257          print_error('sessioncookiesdisable');
    3258      }
    3259  
    3260      // Extra safety.
    3261      \core\session\manager::write_close();
    3262  
    3263      if (null === $keyvalue) {
    3264          $keyvalue = required_param('key', PARAM_ALPHANUM);
    3265      }
    3266  
    3267      $key = validate_user_key($keyvalue, $script, $instance);
    3268  
    3269      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
    3270          print_error('invaliduserid');
    3271      }
    3272  
    3273      core_user::require_active_user($user, true, true);
    3274  
    3275      // Emulate normal session.
    3276      enrol_check_plugins($user);
    3277      \core\session\manager::set_user($user);
    3278  
    3279      // Note we are not using normal login.
    3280      if (!defined('USER_KEY_LOGIN')) {
    3281          define('USER_KEY_LOGIN', true);
    3282      }
    3283  
    3284      // Return instance id - it might be empty.
    3285      return $key->instance;
    3286  }
    3287  
    3288  /**
    3289   * Creates a new private user access key.
    3290   *
    3291   * @param string $script unique target identifier
    3292   * @param int $userid
    3293   * @param int $instance optional instance id
    3294   * @param string $iprestriction optional ip restricted access
    3295   * @param int $validuntil key valid only until given data
    3296   * @return string access key value
    3297   */
    3298  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
    3299      global $DB;
    3300  
    3301      $key = new stdClass();
    3302      $key->script        = $script;
    3303      $key->userid        = $userid;
    3304      $key->instance      = $instance;
    3305      $key->iprestriction = $iprestriction;
    3306      $key->validuntil    = $validuntil;
    3307      $key->timecreated   = time();
    3308  
    3309      // Something long and unique.
    3310      $key->value         = md5($userid.'_'.time().random_string(40));
    3311      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
    3312          // Must be unique.
    3313          $key->value     = md5($userid.'_'.time().random_string(40));
    3314      }
    3315      $DB->insert_record('user_private_key', $key);
    3316      return $key->value;
    3317  }
    3318  
    3319  /**
    3320   * Delete the user's new private user access keys for a particular script.
    3321   *
    3322   * @param string $script unique target identifier
    3323   * @param int $userid
    3324   * @return void
    3325   */
    3326  function delete_user_key($script, $userid) {
    3327      global $DB;
    3328      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
    3329  }
    3330  
    3331  /**
    3332   * Gets a private user access key (and creates one if one doesn't exist).
    3333   *
    3334   * @param string $script unique target identifier
    3335   * @param int $userid
    3336   * @param int $instance optional instance id
    3337   * @param string $iprestriction optional ip restricted access
    3338   * @param int $validuntil key valid only until given date
    3339   * @return string access key value
    3340   */
    3341  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
    3342      global $DB;
    3343  
    3344      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
    3345                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
    3346                                                           'validuntil' => $validuntil))) {
    3347          return $key->value;
    3348      } else {
    3349          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
    3350      }
    3351  }
    3352  
    3353  
    3354  /**
    3355   * Modify the user table by setting the currently logged in user's last login to now.
    3356   *
    3357   * @return bool Always returns true
    3358   */
    3359  function update_user_login_times() {
    3360      global $USER, $DB;
    3361  
    3362      if (isguestuser()) {
    3363          // Do not update guest access times/ips for performance.
    3364          return true;
    3365      }
    3366  
    3367      $now = time();
    3368  
    3369      $user = new stdClass();
    3370      $user->id = $USER->id;
    3371  
    3372      // Make sure all users that logged in have some firstaccess.
    3373      if ($USER->firstaccess == 0) {
    3374          $USER->firstaccess = $user->firstaccess = $now;
    3375      }
    3376  
    3377      // Store the previous current as lastlogin.
    3378      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
    3379  
    3380      $USER->currentlogin = $user->currentlogin = $now;
    3381  
    3382      // Function user_accesstime_log() may not update immediately, better do it here.
    3383      $USER->lastaccess = $user->lastaccess = $now;
    3384      $USER->lastip = $user->lastip = getremoteaddr();
    3385  
    3386      // Note: do not call user_update_user() here because this is part of the login process,
    3387      //       the login event means that these fields were updated.
    3388      $DB->update_record('user', $user);
    3389      return true;
    3390  }
    3391  
    3392  /**
    3393   * Determines if a user has completed setting up their account.
    3394   *
    3395   * The lax mode (with $strict = false) has been introduced for special cases
    3396   * only where we want to skip certain checks intentionally. This is valid in
    3397   * certain mnet or ajax scenarios when the user cannot / should not be
    3398   * redirected to edit their profile. In most cases, you should perform the
    3399   * strict check.
    3400   *
    3401   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
    3402   * @param bool $strict Be more strict and assert id and custom profile fields set, too
    3403   * @return bool
    3404   */
    3405  function user_not_fully_set_up($user, $strict = true) {
    3406      global $CFG;
    3407      require_once($CFG->dirroot.'/user/profile/lib.php');
    3408  
    3409      if (isguestuser($user)) {
    3410          return false;
    3411      }
    3412  
    3413      if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
    3414          return true;
    3415      }
    3416  
    3417      if ($strict) {
    3418          if (empty($user->id)) {
    3419              // Strict mode can be used with existing accounts only.
    3420              return true;
    3421          }
    3422          if (!profile_has_required_custom_fields_set($user->id)) {
    3423              return true;
    3424          }
    3425      }
    3426  
    3427      return false;
    3428  }
    3429  
    3430  /**
    3431   * Check whether the user has exceeded the bounce threshold
    3432   *
    3433   * @param stdClass $user A {@link $USER} object
    3434   * @return bool true => User has exceeded bounce threshold
    3435   */
    3436  function over_bounce_threshold($user) {
    3437      global $CFG, $DB;
    3438  
    3439      if (empty($CFG->handlebounces)) {
    3440          return false;
    3441      }
    3442  
    3443      if (empty($user->id)) {
    3444          // No real (DB) user, nothing to do here.
    3445          return false;
    3446      }
    3447  
    3448      // Set sensible defaults.
    3449      if (empty($CFG->minbounces)) {
    3450          $CFG->minbounces = 10;
    3451      }
    3452      if (empty($CFG->bounceratio)) {
    3453          $CFG->bounceratio = .20;
    3454      }
    3455      $bouncecount = 0;
    3456      $sendcount = 0;
    3457      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
    3458          $bouncecount = $bounce->value;
    3459      }
    3460      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
    3461          $sendcount = $send->value;
    3462      }
    3463      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
    3464  }
    3465  
    3466  /**
    3467   * Used to increment or reset email sent count
    3468   *
    3469   * @param stdClass $user object containing an id
    3470   * @param bool $reset will reset the count to 0
    3471   * @return void
    3472   */
    3473  function set_send_count($user, $reset=false) {
    3474      global $DB;
    3475  
    3476      if (empty($user->id)) {
    3477          // No real (DB) user, nothing to do here.
    3478          return;
    3479      }
    3480  
    3481      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
    3482          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
    3483          $DB->update_record('user_preferences', $pref);
    3484      } else if (!empty($reset)) {
    3485          // If it's not there and we're resetting, don't bother. Make a new one.
    3486          $pref = new stdClass();
    3487          $pref->name   = 'email_send_count';
    3488          $pref->value  = 1;
    3489          $pref->userid = $user->id;
    3490          $DB->insert_record('user_preferences', $pref, false);
    3491      }
    3492  }
    3493  
    3494  /**
    3495   * Increment or reset user's email bounce count
    3496   *
    3497   * @param stdClass $user object containing an id
    3498   * @param bool $reset will reset the count to 0
    3499   */
    3500  function set_bounce_count($user, $reset=false) {
    3501      global $DB;
    3502  
    3503      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
    3504          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
    3505          $DB->update_record('user_preferences', $pref);
    3506      } else if (!empty($reset)) {
    3507          // If it's not there and we're resetting, don't bother. Make a new one.
    3508          $pref = new stdClass();
    3509          $pref->name   = 'email_bounce_count';
    3510          $pref->value  = 1;
    3511          $pref->userid = $user->id;
    3512          $DB->insert_record('user_preferences', $pref, false);
    3513      }
    3514  }
    3515  
    3516  /**
    3517   * Determines if the logged in user is currently moving an activity
    3518   *
    3519   * @param int $courseid The id of the course being tested
    3520   * @return bool
    3521   */
    3522  function ismoving($courseid) {
    3523      global $USER;
    3524  
    3525      if (!empty($USER->activitycopy)) {
    3526          return ($USER->activitycopycourse == $courseid);
    3527      }
    3528      return false;
    3529  }
    3530  
    3531  /**
    3532   * Returns a persons full name
    3533   *
    3534   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
    3535   * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
    3536   * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
    3537   * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
    3538   *
    3539   * @param stdClass $user A {@link $USER} object to get full name of.
    3540   * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
    3541   * @return string
    3542   */
    3543  function fullname($user, $override=false) {
    3544      global $CFG, $SESSION;
    3545  
    3546      if (!isset($user->firstname) and !isset($user->lastname)) {
    3547          return '';
    3548      }
    3549  
    3550      // Get all of the name fields.
    3551      $allnames = \core_user\fields::get_name_fields();
    3552      if ($CFG->debugdeveloper) {
    3553          foreach ($allnames as $allname) {
    3554              if (!property_exists($user, $allname)) {
    3555                  // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
    3556                  debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
    3557                  // Message has been sent, no point in sending the message multiple times.
    3558                  break;
    3559              }
    3560          }
    3561      }
    3562  
    3563      if (!$override) {
    3564          if (!empty($CFG->forcefirstname)) {
    3565              $user->firstname = $CFG->forcefirstname;
    3566          }
    3567          if (!empty($CFG->forcelastname)) {
    3568              $user->lastname = $CFG->forcelastname;
    3569          }
    3570      }
    3571  
    3572      if (!empty($SESSION->fullnamedisplay)) {
    3573          $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
    3574      }
    3575  
    3576      $template = null;
    3577      // If the fullnamedisplay setting is available, set the template to that.
    3578      if (isset($CFG->fullnamedisplay)) {
    3579          $template = $CFG->fullnamedisplay;
    3580      }
    3581      // If the template is empty, or set to language, return the language string.
    3582      if ((empty($template) || $template == 'language') && !$override) {
    3583          return get_string('fullnamedisplay', null, $user);
    3584      }
    3585  
    3586      // Check to see if we are displaying according to the alternative full name format.
    3587      if ($override) {
    3588          if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
    3589              // Default to show just the user names according to the fullnamedisplay string.
    3590              return get_string('fullnamedisplay', null, $user);
    3591          } else {
    3592              // If the override is true, then change the template to use the complete name.
    3593              $template = $CFG->alternativefullnameformat;
    3594          }
    3595      }
    3596  
    3597      $requirednames = array();
    3598      // With each name, see if it is in the display name template, and add it to the required names array if it is.
    3599      foreach ($allnames as $allname) {
    3600          if (strpos($template, $allname) !== false) {
    3601              $requirednames[] = $allname;
    3602          }
    3603      }
    3604  
    3605      $displayname = $template;
    3606      // Switch in the actual data into the template.
    3607      foreach ($requirednames as $altname) {
    3608          if (isset($user->$altname)) {
    3609              // Using empty() on the below if statement causes breakages.
    3610              if ((string)$user->$altname == '') {
    3611                  $displayname = str_replace($altname, 'EMPTY', $displayname);
    3612              } else {
    3613                  $displayname = str_replace($altname, $user->$altname, $displayname);
    3614              }
    3615          } else {
    3616              $displayname = str_replace($altname, 'EMPTY', $displayname);
    3617          }
    3618      }
    3619      // Tidy up any misc. characters (Not perfect, but gets most characters).
    3620      // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
    3621      // katakana and parenthesis.
    3622      $patterns = array();
    3623      // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
    3624      // filled in by a user.
    3625      // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
    3626      $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
    3627      // This regular expression is to remove any double spaces in the display name.
    3628      $patterns[] = '/\s{2,}/u';
    3629      foreach ($patterns as $pattern) {
    3630          $displayname = preg_replace($pattern, ' ', $displayname);
    3631      }
    3632  
    3633      // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
    3634      $displayname = trim($displayname);
    3635      if (empty($displayname)) {
    3636          // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
    3637          // people in general feel is a good setting to fall back on.
    3638          $displayname = $user->firstname;
    3639      }
    3640      return $displayname;
    3641  }
    3642  
    3643  /**
    3644   * Reduces lines of duplicated code for getting user name fields.
    3645   *
    3646   * See also {@link user_picture::unalias()}
    3647   *
    3648   * @param object $addtoobject Object to add user name fields to.
    3649   * @param object $secondobject Object that contains user name field information.
    3650   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
    3651   * @param array $additionalfields Additional fields to be matched with data in the second object.
    3652   * The key can be set to the user table field name.
    3653   * @return object User name fields.
    3654   */
    3655  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
    3656      $fields = [];
    3657      foreach (\core_user\fields::get_name_fields() as $field) {
    3658          $fields[$field] = $prefix . $field;
    3659      }
    3660      if ($additionalfields) {
    3661          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
    3662          // the key is a number and then sets the key to the array value.
    3663          foreach ($additionalfields as $key => $value) {
    3664              if (is_numeric($key)) {
    3665                  $additionalfields[$value] = $prefix . $value;
    3666                  unset($additionalfields[$key]);
    3667              } else {
    3668                  $additionalfields[$key] = $prefix . $value;
    3669              }
    3670          }
    3671          $fields = array_merge($fields, $additionalfields);
    3672      }
    3673      foreach ($fields as $key => $field) {
    3674          // Important that we have all of the user name fields present in the object that we are sending back.
    3675          $addtoobject->$key = '';
    3676          if (isset($secondobject->$field)) {
    3677              $addtoobject->$key = $secondobject->$field;
    3678          }
    3679      }
    3680      return $addtoobject;
    3681  }
    3682  
    3683  /**
    3684   * Returns an array of values in order of occurance in a provided string.
    3685   * The key in the result is the character postion in the string.
    3686   *
    3687   * @param array $values Values to be found in the string format
    3688   * @param string $stringformat The string which may contain values being searched for.
    3689   * @return array An array of values in order according to placement in the string format.
    3690   */
    3691  function order_in_string($values, $stringformat) {
    3692      $valuearray = array();
    3693      foreach ($values as $value) {
    3694          $pattern = "/$value\b/";
    3695          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
    3696          if (preg_match($pattern, $stringformat)) {
    3697              $replacement = "thing";
    3698              // Replace the value with something more unique to ensure we get the right position when using strpos().
    3699              $newformat = preg_replace($pattern, $replacement, $stringformat);
    3700              $position = strpos($newformat, $replacement);
    3701              $valuearray[$position] = $value;
    3702          }
    3703      }
    3704      ksort($valuearray);
    3705      return $valuearray;
    3706  }
    3707  
    3708  /**
    3709   * Returns whether a given authentication plugin exists.
    3710   *
    3711   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
    3712   * @return boolean Whether the plugin is available.
    3713   */
    3714  function exists_auth_plugin($auth) {
    3715      global $CFG;
    3716  
    3717      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
    3718          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
    3719      }
    3720      return false;
    3721  }
    3722  
    3723  /**
    3724   * Checks if a given plugin is in the list of enabled authentication plugins.
    3725   *
    3726   * @param string $auth Authentication plugin.
    3727   * @return boolean Whether the plugin is enabled.
    3728   */
    3729  function is_enabled_auth($auth) {
    3730      if (empty($auth)) {
    3731          return false;
    3732      }
    3733  
    3734      $enabled = get_enabled_auth_plugins();
    3735  
    3736      return in_array($auth, $enabled);
    3737  }
    3738  
    3739  /**
    3740   * Returns an authentication plugin instance.
    3741   *
    3742   * @param string $auth name of authentication plugin
    3743   * @return auth_plugin_base An instance of the required authentication plugin.
    3744   */
    3745  function get_auth_plugin($auth) {
    3746      global $CFG;
    3747  
    3748      // Check the plugin exists first.
    3749      if (! exists_auth_plugin($auth)) {
    3750          print_error('authpluginnotfound', 'debug', '', $auth);
    3751      }
    3752  
    3753      // Return auth plugin instance.
    3754      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
    3755      $class = "auth_plugin_$auth";
    3756      return new $class;
    3757  }
    3758  
    3759  /**
    3760   * Returns array of active auth plugins.
    3761   *
    3762   * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
    3763   * @return array
    3764   */
    3765  function get_enabled_auth_plugins($fix=false) {
    3766      global $CFG;
    3767  
    3768      $default = array('manual', 'nologin');
    3769  
    3770      if (empty($CFG->auth)) {
    3771          $auths = array();
    3772      } else {
    3773          $auths = explode(',', $CFG->auth);
    3774      }
    3775  
    3776      $auths = array_unique($auths);
    3777      $oldauthconfig = implode(',', $auths);
    3778      foreach ($auths as $k => $authname) {
    3779          if (in_array($authname, $default)) {
    3780              // The manual and nologin plugin never need to be stored.
    3781              unset($auths[$k]);
    3782          } else if (!exists_auth_plugin($authname)) {
    3783              debugging(get_string('authpluginnotfound', 'debug', $authname));
    3784              unset($auths[$k]);
    3785          }
    3786      }
    3787  
    3788      // Ideally only explicit interaction from a human admin should trigger a
    3789      // change in auth config, see MDL-70424 for details.
    3790      if ($fix) {
    3791          $newconfig = implode(',', $auths);
    3792          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
    3793              add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
    3794              set_config('auth', $newconfig);
    3795          }
    3796      }
    3797  
    3798      return (array_merge($default, $auths));
    3799  }
    3800  
    3801  /**
    3802   * Returns true if an internal authentication method is being used.
    3803   * if method not specified then, global default is assumed
    3804   *
    3805   * @param string $auth Form of authentication required
    3806   * @return bool
    3807   */
    3808  function is_internal_auth($auth) {
    3809      // Throws error if bad $auth.
    3810      $authplugin = get_auth_plugin($auth);
    3811      return $authplugin->is_internal();
    3812  }
    3813  
    3814  /**
    3815   * Returns true if the user is a 'restored' one.
    3816   *
    3817   * Used in the login process to inform the user and allow him/her to reset the password
    3818   *
    3819   * @param string $username username to be checked
    3820   * @return bool
    3821   */
    3822  function is_restored_user($username) {
    3823      global $CFG, $DB;
    3824  
    3825      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
    3826  }
    3827  
    3828  /**
    3829   * Returns an array of user fields
    3830   *
    3831   * @return array User field/column names
    3832   */
    3833  function get_user_fieldnames() {
    3834      global $DB;
    3835  
    3836      $fieldarray = $DB->get_columns('user');
    3837      unset($fieldarray['id']);
    3838      $fieldarray = array_keys($fieldarray);
    3839  
    3840      return $fieldarray;
    3841  }
    3842  
    3843  /**
    3844   * Returns the string of the language for the new user.
    3845   *
    3846   * @return string language for the new user
    3847   */
    3848  function get_newuser_language() {
    3849      global $CFG, $SESSION;
    3850      return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
    3851  }
    3852  
    3853  /**
    3854   * Creates a bare-bones user record
    3855   *
    3856   * @todo Outline auth types and provide code example
    3857   *
    3858   * @param string $username New user's username to add to record
    3859   * @param string $password New user's password to add to record
    3860   * @param string $auth Form of authentication required
    3861   * @return stdClass A complete user object
    3862   */
    3863  function create_user_record($username, $password, $auth = 'manual') {
    3864      global $CFG, $DB, $SESSION;
    3865      require_once($CFG->dirroot.'/user/profile/lib.php');
    3866      require_once($CFG->dirroot.'/user/lib.php');
    3867  
    3868      // Just in case check text case.
    3869      $username = trim(core_text::strtolower($username));
    3870  
    3871      $authplugin = get_auth_plugin($auth);
    3872      $customfields = $authplugin->get_custom_user_profile_fields();
    3873      $newuser = new stdClass();
    3874      if ($newinfo = $authplugin->get_userinfo($username)) {
    3875          $newinfo = truncate_userinfo($newinfo);
    3876          foreach ($newinfo as $key => $value) {
    3877              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
    3878                  $newuser->$key = $value;
    3879              }
    3880          }
    3881      }
    3882  
    3883      if (!empty($newuser->email)) {
    3884          if (email_is_not_allowed($newuser->email)) {
    3885              unset($newuser->email);
    3886          }
    3887      }
    3888  
    3889      $newuser->auth = $auth;
    3890      $newuser->username = $username;
    3891  
    3892      // Fix for MDL-8480
    3893      // user CFG lang for user if $newuser->lang is empty
    3894      // or $user->lang is not an installed language.
    3895      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
    3896          $newuser->lang = get_newuser_language();
    3897      }
    3898      $newuser->confirmed = 1;
    3899      $newuser->lastip = getremoteaddr();
    3900      $newuser->timecreated = time();
    3901      $newuser->timemodified = $newuser->timecreated;
    3902      $newuser->mnethostid = $CFG->mnet_localhost_id;
    3903  
    3904      $newuser->id = user_create_user($newuser, false, false);
    3905  
    3906      // Save user profile data.
    3907      profile_save_data($newuser);
    3908  
    3909      $user = get_complete_user_data('id', $newuser->id);
    3910      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
    3911          set_user_preference('auth_forcepasswordchange', 1, $user);
    3912      }
    3913      // Set the password.
    3914      update_internal_user_password($user, $password);
    3915  
    3916      // Trigger event.
    3917      \core\event\user_created::create_from_userid($newuser->id)->trigger();
    3918  
    3919      return $user;
    3920  }
    3921  
    3922  /**
    3923   * Will update a local user record from an external source (MNET users can not be updated using this method!).
    3924   *
    3925   * @param string $username user's username to update the record
    3926   * @return stdClass A complete user object
    3927   */
    3928  function update_user_record($username) {
    3929      global $DB, $CFG;
    3930      // Just in case check text case.
    3931      $username = trim(core_text::strtolower($username));
    3932  
    3933      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
    3934      return update_user_record_by_id($oldinfo->id);
    3935  }
    3936  
    3937  /**
    3938   * Will update a local user record from an external source (MNET users can not be updated using this method!).
    3939   *
    3940   * @param int $id user id
    3941   * @return stdClass A complete user object
    3942   */
    3943  function update_user_record_by_id($id) {
    3944      global $DB, $CFG;
    3945      require_once($CFG->dirroot."/user/profile/lib.php");
    3946      require_once($CFG->dirroot.'/user/lib.php');
    3947  
    3948      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
    3949      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
    3950  
    3951      $newuser = array();
    3952      $userauth = get_auth_plugin($oldinfo->auth);
    3953  
    3954      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
    3955          $newinfo = truncate_userinfo($newinfo);
    3956          $customfields = $userauth->get_custom_user_profile_fields();
    3957  
    3958          foreach ($newinfo as $key => $value) {
    3959              $iscustom = in_array($key, $customfields);
    3960              if (!$iscustom) {
    3961                  $key = strtolower($key);
    3962              }
    3963              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
    3964                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
    3965                  // Unknown or must not be changed.
    3966                  continue;
    3967              }
    3968              if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
    3969                  continue;
    3970              }
    3971              $confval = $userauth->config->{'field_updatelocal_' . $key};
    3972              $lockval = $userauth->config->{'field_lock_' . $key};
    3973              if ($confval === 'onlogin') {
    3974                  // MDL-4207 Don't overwrite modified user profile values with
    3975                  // empty LDAP values when 'unlocked if empty' is set. The purpose
    3976                  // of the setting 'unlocked if empty' is to allow the user to fill
    3977                  // in a value for the selected field _if LDAP is giving
    3978                  // nothing_ for this field. Thus it makes sense to let this value
    3979                  // stand in until LDAP is giving a value for this field.
    3980                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
    3981                      if ($iscustom || (in_array($key, $userauth->userfields) &&
    3982                              ((string)$oldinfo->$key !== (string)$value))) {
    3983                          $newuser[$key] = (string)$value;
    3984                      }
    3985                  }
    3986              }
    3987          }
    3988          if ($newuser) {
    3989              $newuser['id'] = $oldinfo->id;
    3990              $newuser['timemodified'] = time();
    3991              user_update_user((object) $newuser, false, false);
    3992  
    3993              // Save user profile data.
    3994              profile_save_data((object) $newuser);
    3995  
    3996              // Trigger event.
    3997              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
    3998          }
    3999      }
    4000  
    4001      return get_complete_user_data('id', $oldinfo->id);
    4002  }
    4003  
    4004  /**
    4005   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
    4006   *
    4007   * @param array $info Array of user properties to truncate if needed
    4008   * @return array The now truncated information that was passed in
    4009   */
    4010  function truncate_userinfo(array $info) {
    4011      // Define the limits.
    4012      $limit = array(
    4013          'username'    => 100,
    4014          'idnumber'    => 255,
    4015          'firstname'   => 100,
    4016          'lastname'    => 100,
    4017          'email'       => 100,
    4018          'phone1'      =>  20,
    4019          'phone2'      =>  20,
    4020          'institution' => 255,
    4021          'department'  => 255,
    4022          'address'     => 255,
    4023          'city'        => 120,
    4024          'country'     =>   2,
    4025      );
    4026  
    4027      // Apply where needed.
    4028      foreach (array_keys($info) as $key) {
    4029          if (!empty($limit[$key])) {
    4030              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
    4031          }
    4032      }
    4033  
    4034      return $info;
    4035  }
    4036  
    4037  /**
    4038   * Marks user deleted in internal user database and notifies the auth plugin.
    4039   * Also unenrols user from all roles and does other cleanup.
    4040   *
    4041   * Any plugin that needs to purge user data should register the 'user_deleted' event.
    4042   *
    4043   * @param stdClass $user full user object before delete
    4044   * @return boolean success
    4045   * @throws coding_exception if invalid $user parameter detected
    4046   */
    4047  function delete_user(stdClass $user) {
    4048      global $CFG, $DB, $SESSION;
    4049      require_once($CFG->libdir.'/grouplib.php');
    4050      require_once($CFG->libdir.'/gradelib.php');
    4051      require_once($CFG->dirroot.'/message/lib.php');
    4052      require_once($CFG->dirroot.'/user/lib.php');
    4053  
    4054      // Make sure nobody sends bogus record type as parameter.
    4055      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
    4056          throw new coding_exception('Invalid $user parameter in delete_user() detected');
    4057      }
    4058  
    4059      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
    4060      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
    4061          debugging('Attempt to delete unknown user account.');
    4062          return false;
    4063      }
    4064  
    4065      // There must be always exactly one guest record, originally the guest account was identified by username only,
    4066      // now we use $CFG->siteguest for performance reasons.
    4067      if ($user->username === 'guest' or isguestuser($user)) {
    4068          debugging('Guest user account can not be deleted.');
    4069          return false;
    4070      }
    4071  
    4072      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
    4073      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
    4074      if ($user->auth === 'manual' and is_siteadmin($user)) {
    4075          debugging('Local administrator accounts can not be deleted.');
    4076          return false;
    4077      }
    4078  
    4079      // Allow plugins to use this user object before we completely delete it.
    4080      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
    4081          foreach ($pluginsfunction as $plugintype => $plugins) {
    4082              foreach ($plugins as $pluginfunction) {
    4083                  $pluginfunction($user);
    4084              }
    4085          }
    4086      }
    4087  
    4088      // Keep user record before updating it, as we have to pass this to user_deleted event.
    4089      $olduser = clone $user;
    4090  
    4091      // Keep a copy of user context, we need it for event.
    4092      $usercontext = context_user::instance($user->id);
    4093  
    4094      // Delete all grades - backup is kept in grade_grades_history table.
    4095      grade_user_delete($user->id);
    4096  
    4097      // TODO: remove from cohorts using standard API here.
    4098  
    4099      // Remove user tags.
    4100      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
    4101  
    4102      // Unconditionally unenrol from all courses.
    4103      enrol_user_delete($user);
    4104  
    4105      // Unenrol from all roles in all contexts.
    4106      // This might be slow but it is really needed - modules might do some extra cleanup!
    4107      role_unassign_all(array('userid' => $user->id));
    4108  
    4109      // Notify the competency subsystem.
    4110      \core_competency\api::hook_user_deleted($user->id);
    4111  
    4112      // Now do a brute force cleanup.
    4113  
    4114      // Delete all user events and subscription events.
    4115      $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
    4116  
    4117      // Now, delete all calendar subscription from the user.
    4118      $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
    4119  
    4120      // Remove from all cohorts.
    4121      $DB->delete_records('cohort_members', array('userid' => $user->id));
    4122  
    4123      // Remove from all groups.
    4124      $DB->delete_records('groups_members', array('userid' => $user->id));
    4125  
    4126      // Brute force unenrol from all courses.
    4127      $DB->delete_records('user_enrolments', array('userid' => $user->id));
    4128  
    4129      // Purge user preferences.
    4130      $DB->delete_records('user_preferences', array('userid' => $user->id));
    4131  
    4132      // Purge user extra profile info.
    4133      $DB->delete_records('user_info_data', array('userid' => $user->id));
    4134  
    4135      // Purge log of previous password hashes.
    4136      $DB->delete_records('user_password_history', array('userid' => $user->id));
    4137  
    4138      // Last course access not necessary either.
    4139      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
    4140      // Remove all user tokens.
    4141      $DB->delete_records('external_tokens', array('userid' => $user->id));
    4142  
    4143      // Unauthorise the user for all services.
    4144      $DB->delete_records('external_services_users', array('userid' => $user->id));
    4145  
    4146      // Remove users private keys.
    4147      $DB->delete_records('user_private_key', array('userid' => $user->id));
    4148  
    4149      // Remove users customised pages.
    4150      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
    4151  
    4152      // Remove user's oauth2 refresh tokens, if present.
    4153      $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
    4154  
    4155      // Delete user from $SESSION->bulk_users.
    4156      if (isset($SESSION->bulk_users[$user->id])) {
    4157          unset($SESSION->bulk_users[$user->id]);
    4158      }
    4159  
    4160      // Force logout - may fail if file based sessions used, sorry.
    4161      \core\session\manager::kill_user_sessions($user->id);
    4162  
    4163      // Generate username from email address, or a fake email.
    4164      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
    4165  
    4166      $deltime = time();
    4167      $deltimelength = core_text::strlen((string) $deltime);
    4168  
    4169      // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
    4170      $delname = clean_param($delemail, PARAM_USERNAME);
    4171      $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
    4172  
    4173      // Workaround for bulk deletes of users with the same email address.
    4174      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
    4175          $delname++;
    4176      }
    4177  
    4178      // Mark internal user record as "deleted".
    4179      $updateuser = new stdClass();
    4180      $updateuser->id           = $user->id;
    4181      $updateuser->deleted      = 1;
    4182      $updateuser->username     = $delname;            // Remember it just in case.
    4183      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
    4184      $updateuser->idnumber     = '';                  // Clear this field to free it up.
    4185      $updateuser->picture      = 0;
    4186      $updateuser->timemodified = $deltime;
    4187  
    4188      // Don't trigger update event, as user is being deleted.
    4189      user_update_user($updateuser, false, false);
    4190  
    4191      // Delete all content associated with the user context, but not the context itself.
    4192      $usercontext->delete_content();
    4193  
    4194      // Delete any search data.
    4195      \core_search\manager::context_deleted($usercontext);
    4196  
    4197      // Any plugin that needs to cleanup should register this event.
    4198      // Trigger event.
    4199      $event = \core\event\user_deleted::create(
    4200              array(
    4201                  'objectid' => $user->id,
    4202                  'relateduserid' => $user->id,
    4203                  'context' => $usercontext,
    4204                  'other' => array(
    4205                      'username' => $user->username,
    4206                      'email' => $user->email,
    4207                      'idnumber' => $user->idnumber,
    4208                      'picture' => $user->picture,
    4209                      'mnethostid' => $user->mnethostid
    4210                      )
    4211                  )
    4212              );
    4213      $event->add_record_snapshot('user', $olduser);
    4214      $event->trigger();
    4215  
    4216      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
    4217      // should know about this updated property persisted to the user's table.
    4218      $user->timemodified = $updateuser->timemodified;
    4219  
    4220      // Notify auth plugin - do not block the delete even when plugin fails.
    4221      $authplugin = get_auth_plugin($user->auth);
    4222      $authplugin->user_delete($user);
    4223  
    4224      return true;
    4225  }
    4226  
    4227  /**
    4228   * Retrieve the guest user object.
    4229   *
    4230   * @return stdClass A {@link $USER} object
    4231   */
    4232  function guest_user() {
    4233      global $CFG, $DB;
    4234  
    4235      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
    4236          $newuser->confirmed = 1;
    4237          $newuser->lang = get_newuser_language();
    4238          $newuser->lastip = getremoteaddr();
    4239      }
    4240  
    4241      return $newuser;
    4242  }
    4243  
    4244  /**
    4245   * Authenticates a user against the chosen authentication mechanism
    4246   *
    4247   * Given a username and password, this function looks them
    4248   * up using the currently selected authentication mechanism,
    4249   * and if the authentication is successful, it returns a
    4250   * valid $user object from the 'user' table.
    4251   *
    4252   * Uses auth_ functions from the currently active auth module
    4253   *
    4254   * After authenticate_user_login() returns success, you will need to
    4255   * log that the user has logged in, and call complete_user_login() to set
    4256   * the session up.
    4257   *
    4258   * Note: this function works only with non-mnet accounts!
    4259   *
    4260   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
    4261   * @param string $password  User's password
    4262   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
    4263   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
    4264   * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
    4265   * @return stdClass|false A {@link $USER} object or false if error
    4266   */
    4267  function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
    4268      global $CFG, $DB, $PAGE;
    4269      require_once("$CFG->libdir/authlib.php");
    4270  
    4271      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
    4272          // we have found the user
    4273  
    4274      } else if (!empty($CFG->authloginviaemail)) {
    4275          if ($email = clean_param($username, PARAM_EMAIL)) {
    4276              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
    4277              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
    4278              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
    4279              if (count($users) === 1) {
    4280                  // Use email for login only if unique.
    4281                  $user = reset($users);
    4282                  $user = get_complete_user_data('id', $user->id);
    4283                  $username = $user->username;
    4284              }
    4285              unset($users);
    4286          }
    4287      }
    4288  
    4289      // Make sure this request came from the login form.
    4290      if (!\core\session\manager::validate_login_token($logintoken)) {
    4291          $failurereason = AUTH_LOGIN_FAILED;
    4292  
    4293          // Trigger login failed event (specifying the ID of the found user, if available).
    4294          \core\event\user_login_failed::create([
    4295              'userid' => ($user->id ?? 0),
    4296              'other' => [
    4297                  'username' => $username,
    4298                  'reason' => $failurereason,
    4299              ],
    4300          ])->trigger();
    4301  
    4302          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
    4303          return false;
    4304      }
    4305  
    4306      $authsenabled = get_enabled_auth_plugins();
    4307  
    4308      if ($user) {
    4309          // Use manual if auth not set.
    4310          $auth = empty($user->auth) ? 'manual' : $user->auth;
    4311  
    4312          if (in_array($user->auth, $authsenabled)) {
    4313              $authplugin = get_auth_plugin($user->auth);
    4314              $authplugin->pre_user_login_hook($user);
    4315          }
    4316  
    4317          if (!empty($user->suspended)) {
    4318              $failurereason = AUTH_LOGIN_SUSPENDED;
    4319  
    4320              // Trigger login failed event.
    4321              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
    4322                      'other' => array('username' => $username, 'reason' => $failurereason)));
    4323              $event->trigger();
    4324              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
    4325              return false;
    4326          }
    4327          if ($auth=='nologin' or !is_enabled_auth($auth)) {
    4328              // Legacy way to suspend user.
    4329              $failurereason = AUTH_LOGIN_SUSPENDED;
    4330  
    4331              // Trigger login failed event.
    4332              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
    4333                      'other' => array('username' => $username, 'reason' => $failurereason)));
    4334              $event->trigger();
    4335              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
    4336              return false;
    4337          }
    4338          $auths = array($auth);
    4339  
    4340      } else {
    4341          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
    4342          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
    4343              $failurereason = AUTH_LOGIN_NOUSER;
    4344  
    4345              // Trigger login failed event.
    4346              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
    4347                      'reason' => $failurereason)));
    4348              $event->trigger();
    4349              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
    4350              return false;
    4351          }
    4352  
    4353          // User does not exist.
    4354          $auths = $authsenabled;
    4355          $user = new stdClass();
    4356          $user->id = 0;
    4357      }
    4358  
    4359      if ($ignorelockout) {
    4360          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
    4361          // or this function is called from a SSO script.
    4362      } else if ($user->id) {
    4363          // Verify login lockout after other ways that may prevent user login.
    4364          if (login_is_lockedout($user)) {
    4365              $failurereason = AUTH_LOGIN_LOCKOUT;
    4366  
    4367              // Trigger login failed event.
    4368              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
    4369                      'other' => array('username' => $username, 'reason' => $failurereason)));
    4370              $event->trigger();
    4371  
    4372              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
    4373              return false;
    4374          }
    4375      } else {
    4376          // We can not lockout non-existing accounts.
    4377      }
    4378  
    4379      foreach ($auths as $auth) {
    4380          $authplugin = get_auth_plugin($auth);
    4381  
    4382          // On auth fail fall through to the next plugin.
    4383          if (!$authplugin->user_login($username, $password)) {
    4384              continue;
    4385          }
    4386