Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • /lib/ -> moodlelib.php (source)

    Differences Between: [Versions 28 and 29] [Versions 28 and 30] [Versions 28 and 31] [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35] [Versions 28 and 36] [Versions 28 and 37]

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