Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
/lib/ -> moodlelib.php (source)

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * moodlelib.php - Moodle main library
  19   *
  20   * Main library file of miscellaneous general-purpose Moodle functions.
  21   * Other main libraries:
  22   *  - weblib.php      - functions that produce web output
  23   *  - datalib.php     - functions that access the database
  24   *
  25   * @package    core
  26   * @subpackage lib
  27   * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  // CONSTANTS (Encased in phpdoc proper comments).
  34  
  35  // Date and time constants.
  36  /**
  37   * Time constant - the number of seconds in a year
  38   */
  39  define('YEARSECS', 31536000);
  40  
  41  /**
  42   * Time constant - the number of seconds in a week
  43   */
  44  define('WEEKSECS', 604800);
  45  
  46  /**
  47   * Time constant - the number of seconds in a day
  48   */
  49  define('DAYSECS', 86400);
  50  
  51  /**
  52   * Time constant - the number of seconds in an hour
  53   */
  54  define('HOURSECS', 3600);
  55  
  56  /**
  57   * Time constant - the number of seconds in a minute
  58   */
  59  define('MINSECS', 60);
  60  
  61  /**
  62   * Time constant - the number of minutes in a day
  63   */
  64  define('DAYMINS', 1440);
  65  
  66  /**
  67   * Time constant - the number of minutes in an hour
  68   */
  69  define('HOURMINS', 60);
  70  
  71  // Parameter constants - every call to optional_param(), required_param()
  72  // or clean_param() should have a specified type of parameter.
  73  
  74  /**
  75   * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
  76   */
  77  define('PARAM_ALPHA',    'alpha');
  78  
  79  /**
  80   * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
  81   * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
  82   */
  83  define('PARAM_ALPHAEXT', 'alphaext');
  84  
  85  /**
  86   * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
  87   */
  88  define('PARAM_ALPHANUM', 'alphanum');
  89  
  90  /**
  91   * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
  92   */
  93  define('PARAM_ALPHANUMEXT', 'alphanumext');
  94  
  95  /**
  96   * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
  97   */
  98  define('PARAM_AUTH',  'auth');
  99  
 100  /**
 101   * PARAM_BASE64 - Base 64 encoded format
 102   */
 103  define('PARAM_BASE64',   'base64');
 104  
 105  /**
 106   * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
 107   */
 108  define('PARAM_BOOL',     'bool');
 109  
 110  /**
 111   * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
 112   * checked against the list of capabilities in the database.
 113   */
 114  define('PARAM_CAPABILITY',   'capability');
 115  
 116  /**
 117   * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
 118   * to use this. The normal mode of operation is to use PARAM_RAW when receiving
 119   * the input (required/optional_param or formslib) and then sanitise the HTML
 120   * using format_text on output. This is for the rare cases when you want to
 121   * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
 122   */
 123  define('PARAM_CLEANHTML', 'cleanhtml');
 124  
 125  /**
 126   * PARAM_EMAIL - an email address following the RFC
 127   */
 128  define('PARAM_EMAIL',   'email');
 129  
 130  /**
 131   * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
 132   */
 133  define('PARAM_FILE',   'file');
 134  
 135  /**
 136   * PARAM_FLOAT - a real/floating point number.
 137   *
 138   * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
 139   * It does not work for languages that use , as a decimal separator.
 140   * Use PARAM_LOCALISEDFLOAT instead.
 141   */
 142  define('PARAM_FLOAT',  'float');
 143  
 144  /**
 145   * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
 146   * This is preferred over PARAM_FLOAT for numbers typed in by the user.
 147   * Cleans localised numbers to computer readable numbers; false for invalid numbers.
 148   */
 149  define('PARAM_LOCALISEDFLOAT',  'localisedfloat');
 150  
 151  /**
 152   * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
 153   */
 154  define('PARAM_HOST',     'host');
 155  
 156  /**
 157   * PARAM_INT - integers only, use when expecting only numbers.
 158   */
 159  define('PARAM_INT',      'int');
 160  
 161  /**
 162   * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
 163   */
 164  define('PARAM_LANG',  'lang');
 165  
 166  /**
 167   * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
 168   * others! Implies PARAM_URL!)
 169   */
 170  define('PARAM_LOCALURL', 'localurl');
 171  
 172  /**
 173   * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
 174   */
 175  define('PARAM_NOTAGS',   'notags');
 176  
 177  /**
 178   * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
 179   * traversals note: the leading slash is not removed, window drive letter is not allowed
 180   */
 181  define('PARAM_PATH',     'path');
 182  
 183  /**
 184   * PARAM_PEM - Privacy Enhanced Mail format
 185   */
 186  define('PARAM_PEM',      'pem');
 187  
 188  /**
 189   * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
 190   */
 191  define('PARAM_PERMISSION',   'permission');
 192  
 193  /**
 194   * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
 195   */
 196  define('PARAM_RAW', 'raw');
 197  
 198  /**
 199   * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
 200   */
 201  define('PARAM_RAW_TRIMMED', 'raw_trimmed');
 202  
 203  /**
 204   * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
 205   */
 206  define('PARAM_SAFEDIR',  'safedir');
 207  
 208  /**
 209   * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
 210   * and other references to Moodle code files.
 211   *
 212   * This is NOT intended to be used for absolute paths or any user uploaded files.
 213   */
 214  define('PARAM_SAFEPATH',  'safepath');
 215  
 216  /**
 217   * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
 218   */
 219  define('PARAM_SEQUENCE',  'sequence');
 220  
 221  /**
 222   * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
 223   */
 224  define('PARAM_TAG',   'tag');
 225  
 226  /**
 227   * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
 228   */
 229  define('PARAM_TAGLIST',   'taglist');
 230  
 231  /**
 232   * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
 233   */
 234  define('PARAM_TEXT',  'text');
 235  
 236  /**
 237   * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
 238   */
 239  define('PARAM_THEME',  'theme');
 240  
 241  /**
 242   * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
 243   * http://localhost.localdomain/ is ok.
 244   */
 245  define('PARAM_URL',      'url');
 246  
 247  /**
 248   * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
 249   * accounts, do NOT use when syncing with external systems!!
 250   */
 251  define('PARAM_USERNAME',    'username');
 252  
 253  /**
 254   * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
 255   */
 256  define('PARAM_STRINGID',    'stringid');
 257  
 258  // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
 259  /**
 260   * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
 261   * It was one of the first types, that is why it is abused so much ;-)
 262   * @deprecated since 2.0
 263   */
 264  define('PARAM_CLEAN',    'clean');
 265  
 266  /**
 267   * PARAM_INTEGER - deprecated alias for PARAM_INT
 268   * @deprecated since 2.0
 269   */
 270  define('PARAM_INTEGER',  'int');
 271  
 272  /**
 273   * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
 274   * @deprecated since 2.0
 275   */
 276  define('PARAM_NUMBER',  'float');
 277  
 278  /**
 279   * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
 280   * NOTE: originally alias for PARAM_APLHA
 281   * @deprecated since 2.0
 282   */
 283  define('PARAM_ACTION',   'alphanumext');
 284  
 285  /**
 286   * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
 287   * NOTE: originally alias for PARAM_APLHA
 288   * @deprecated since 2.0
 289   */
 290  define('PARAM_FORMAT',   'alphanumext');
 291  
 292  /**
 293   * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
 294   * @deprecated since 2.0
 295   */
 296  define('PARAM_MULTILANG',  'text');
 297  
 298  /**
 299   * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
 300   * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
 301   * America/Port-au-Prince)
 302   */
 303  define('PARAM_TIMEZONE', 'timezone');
 304  
 305  /**
 306   * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
 307   */
 308  define('PARAM_CLEANFILE', 'file');
 309  
 310  /**
 311   * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
 312   * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
 313   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 314   * NOTE: numbers and underscores are strongly discouraged in plugin names!
 315   */
 316  define('PARAM_COMPONENT', 'component');
 317  
 318  /**
 319   * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
 320   * It is usually used together with context id and component.
 321   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 322   */
 323  define('PARAM_AREA', 'area');
 324  
 325  /**
 326   * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
 327   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 328   * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
 329   */
 330  define('PARAM_PLUGIN', 'plugin');
 331  
 332  
 333  // Web Services.
 334  
 335  /**
 336   * VALUE_REQUIRED - if the parameter is not supplied, there is an error
 337   */
 338  define('VALUE_REQUIRED', 1);
 339  
 340  /**
 341   * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
 342   */
 343  define('VALUE_OPTIONAL', 2);
 344  
 345  /**
 346   * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
 347   */
 348  define('VALUE_DEFAULT', 0);
 349  
 350  /**
 351   * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
 352   */
 353  define('NULL_NOT_ALLOWED', false);
 354  
 355  /**
 356   * NULL_ALLOWED - the parameter can be set to null in the database
 357   */
 358  define('NULL_ALLOWED', true);
 359  
 360  // Page types.
 361  
 362  /**
 363   * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
 364   */
 365  define('PAGE_COURSE_VIEW', 'course-view');
 366  
 367  /** Get remote addr constant */
 368  define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
 369  /** Get remote addr constant */
 370  define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
 371  /**
 372   * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
 373   */
 374  define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
 375  
 376  // Blog access level constant declaration.
 377  define ('BLOG_USER_LEVEL', 1);
 378  define ('BLOG_GROUP_LEVEL', 2);
 379  define ('BLOG_COURSE_LEVEL', 3);
 380  define ('BLOG_SITE_LEVEL', 4);
 381  define ('BLOG_GLOBAL_LEVEL', 5);
 382  
 383  
 384  // Tag constants.
 385  /**
 386   * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
 387   * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
 388   * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
 389   *
 390   * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
 391   */
 392  define('TAG_MAX_LENGTH', 50);
 393  
 394  // Password policy constants.
 395  define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
 396  define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
 397  define ('PASSWORD_DIGITS', '0123456789');
 398  define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
 399  
 400  // Feature constants.
 401  // Used for plugin_supports() to report features that are, or are not, supported by a module.
 402  
 403  /** True if module can provide a grade */
 404  define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
 405  /** True if module supports outcomes */
 406  define('FEATURE_GRADE_OUTCOMES', 'outcomes');
 407  /** True if module supports advanced grading methods */
 408  define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
 409  /** True if module controls the grade visibility over the gradebook */
 410  define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
 411  /** True if module supports plagiarism plugins */
 412  define('FEATURE_PLAGIARISM', 'plagiarism');
 413  
 414  /** True if module has code to track whether somebody viewed it */
 415  define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
 416  /** True if module has custom completion rules */
 417  define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
 418  
 419  /** True if module has no 'view' page (like label) */
 420  define('FEATURE_NO_VIEW_LINK', 'viewlink');
 421  /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
 422  define('FEATURE_IDNUMBER', 'idnumber');
 423  /** True if module supports groups */
 424  define('FEATURE_GROUPS', 'groups');
 425  /** True if module supports groupings */
 426  define('FEATURE_GROUPINGS', 'groupings');
 427  /**
 428   * True if module supports groupmembersonly (which no longer exists)
 429   * @deprecated Since Moodle 2.8
 430   */
 431  define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
 432  
 433  /** Type of module */
 434  define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
 435  /** True if module supports intro editor */
 436  define('FEATURE_MOD_INTRO', 'mod_intro');
 437  /** True if module has default completion */
 438  define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
 439  
 440  define('FEATURE_COMMENT', 'comment');
 441  
 442  define('FEATURE_RATE', 'rate');
 443  /** True if module supports backup/restore of moodle2 format */
 444  define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
 445  
 446  /** True if module can show description on course main page */
 447  define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
 448  
 449  /** True if module uses the question bank */
 450  define('FEATURE_USES_QUESTIONS', 'usesquestions');
 451  
 452  /**
 453   * Maximum filename char size
 454   */
 455  define('MAX_FILENAME_SIZE', 100);
 456  
 457  /** Unspecified module archetype */
 458  define('MOD_ARCHETYPE_OTHER', 0);
 459  /** Resource-like type module */
 460  define('MOD_ARCHETYPE_RESOURCE', 1);
 461  /** Assignment module archetype */
 462  define('MOD_ARCHETYPE_ASSIGNMENT', 2);
 463  /** System (not user-addable) module archetype */
 464  define('MOD_ARCHETYPE_SYSTEM', 3);
 465  
 466  /** Type of module */
 467  define('FEATURE_MOD_PURPOSE', 'mod_purpose');
 468  /** Module purpose administration */
 469  define('MOD_PURPOSE_ADMINISTRATION', 'administration');
 470  /** Module purpose assessment */
 471  define('MOD_PURPOSE_ASSESSMENT', 'assessment');
 472  /** Module purpose communication */
 473  define('MOD_PURPOSE_COLLABORATION', 'collaboration');
 474  /** Module purpose communication */
 475  define('MOD_PURPOSE_COMMUNICATION', 'communication');
 476  /** Module purpose content */
 477  define('MOD_PURPOSE_CONTENT', 'content');
 478  /** Module purpose interface */
 479  define('MOD_PURPOSE_INTERFACE', 'interface');
 480  /** Module purpose other */
 481  define('MOD_PURPOSE_OTHER', 'other');
 482  
 483  /**
 484   * Security token used for allowing access
 485   * from external application such as web services.
 486   * Scripts do not use any session, performance is relatively
 487   * low because we need to load access info in each request.
 488   * Scripts are executed in parallel.
 489   */
 490  define('EXTERNAL_TOKEN_PERMANENT', 0);
 491  
 492  /**
 493   * Security token used for allowing access
 494   * of embedded applications, the code is executed in the
 495   * active user session. Token is invalidated after user logs out.
 496   * Scripts are executed serially - normal session locking is used.
 497   */
 498  define('EXTERNAL_TOKEN_EMBEDDED', 1);
 499  
 500  /**
 501   * The home page should be the site home
 502   */
 503  define('HOMEPAGE_SITE', 0);
 504  /**
 505   * The home page should be the users my page
 506   */
 507  define('HOMEPAGE_MY', 1);
 508  /**
 509   * The home page can be chosen by the user
 510   */
 511  define('HOMEPAGE_USER', 2);
 512  /**
 513   * The home page should be the users my courses page
 514   */
 515  define('HOMEPAGE_MYCOURSES', 3);
 516  
 517  /**
 518   * URL of the Moodle sites registration portal.
 519   */
 520  defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
 521  
 522  /**
 523   * URL of the statistic server public key.
 524   */
 525  defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
 526  
 527  /**
 528   * Moodle mobile app service name
 529   */
 530  define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
 531  
 532  /**
 533   * Indicates the user has the capabilities required to ignore activity and course file size restrictions
 534   */
 535  define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 536  
 537  /**
 538   * Course display settings: display all sections on one page.
 539   */
 540  define('COURSE_DISPLAY_SINGLEPAGE', 0);
 541  /**
 542   * Course display settings: split pages into a page per section.
 543   */
 544  define('COURSE_DISPLAY_MULTIPAGE', 1);
 545  
 546  /**
 547   * Authentication constant: String used in password field when password is not stored.
 548   */
 549  define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
 550  
 551  /**
 552   * Email from header to never include via information.
 553   */
 554  define('EMAIL_VIA_NEVER', 0);
 555  
 556  /**
 557   * Email from header to always include via information.
 558   */
 559  define('EMAIL_VIA_ALWAYS', 1);
 560  
 561  /**
 562   * Email from header to only include via information if the address is no-reply.
 563   */
 564  define('EMAIL_VIA_NO_REPLY_ONLY', 2);
 565  
 566  // PARAMETER HANDLING.
 567  
 568  /**
 569   * Returns a particular value for the named variable, taken from
 570   * POST or GET.  If the parameter doesn't exist then an error is
 571   * thrown because we require this variable.
 572   *
 573   * This function should be used to initialise all required values
 574   * in a script that are based on parameters.  Usually it will be
 575   * used like this:
 576   *    $id = required_param('id', PARAM_INT);
 577   *
 578   * Please note the $type parameter is now required and the value can not be array.
 579   *
 580   * @param string $parname the name of the page parameter we want
 581   * @param string $type expected type of parameter
 582   * @return mixed
 583   * @throws coding_exception
 584   */
 585  function required_param($parname, $type) {
 586      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 587          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
 588      }
 589      // POST has precedence.
 590      if (isset($_POST[$parname])) {
 591          $param = $_POST[$parname];
 592      } else if (isset($_GET[$parname])) {
 593          $param = $_GET[$parname];
 594      } else {
 595          print_error('missingparam', '', '', $parname);
 596      }
 597  
 598      if (is_array($param)) {
 599          debugging('Invalid array parameter detected in required_param(): '.$parname);
 600          // TODO: switch to fatal error in Moodle 2.3.
 601          return required_param_array($parname, $type);
 602      }
 603  
 604      return clean_param($param, $type);
 605  }
 606  
 607  /**
 608   * Returns a particular array value for the named variable, taken from
 609   * POST or GET.  If the parameter doesn't exist then an error is
 610   * thrown because we require this variable.
 611   *
 612   * This function should be used to initialise all required values
 613   * in a script that are based on parameters.  Usually it will be
 614   * used like this:
 615   *    $ids = required_param_array('ids', PARAM_INT);
 616   *
 617   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 618   *
 619   * @param string $parname the name of the page parameter we want
 620   * @param string $type expected type of parameter
 621   * @return array
 622   * @throws coding_exception
 623   */
 624  function required_param_array($parname, $type) {
 625      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 626          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
 627      }
 628      // POST has precedence.
 629      if (isset($_POST[$parname])) {
 630          $param = $_POST[$parname];
 631      } else if (isset($_GET[$parname])) {
 632          $param = $_GET[$parname];
 633      } else {
 634          print_error('missingparam', '', '', $parname);
 635      }
 636      if (!is_array($param)) {
 637          print_error('missingparam', '', '', $parname);
 638      }
 639  
 640      $result = array();
 641      foreach ($param as $key => $value) {
 642          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 643              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
 644              continue;
 645          }
 646          $result[$key] = clean_param($value, $type);
 647      }
 648  
 649      return $result;
 650  }
 651  
 652  /**
 653   * Returns a particular value for the named variable, taken from
 654   * POST or GET, otherwise returning a given default.
 655   *
 656   * This function should be used to initialise all optional values
 657   * in a script that are based on parameters.  Usually it will be
 658   * used like this:
 659   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
 660   *
 661   * Please note the $type parameter is now required and the value can not be array.
 662   *
 663   * @param string $parname the name of the page parameter we want
 664   * @param mixed  $default the default value to return if nothing is found
 665   * @param string $type expected type of parameter
 666   * @return mixed
 667   * @throws coding_exception
 668   */
 669  function optional_param($parname, $default, $type) {
 670      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 671          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 672      }
 673  
 674      // POST has precedence.
 675      if (isset($_POST[$parname])) {
 676          $param = $_POST[$parname];
 677      } else if (isset($_GET[$parname])) {
 678          $param = $_GET[$parname];
 679      } else {
 680          return $default;
 681      }
 682  
 683      if (is_array($param)) {
 684          debugging('Invalid array parameter detected in required_param(): '.$parname);
 685          // TODO: switch to $default in Moodle 2.3.
 686          return optional_param_array($parname, $default, $type);
 687      }
 688  
 689      return clean_param($param, $type);
 690  }
 691  
 692  /**
 693   * Returns a particular array value for the named variable, taken from
 694   * POST or GET, otherwise returning a given default.
 695   *
 696   * This function should be used to initialise all optional values
 697   * in a script that are based on parameters.  Usually it will be
 698   * used like this:
 699   *    $ids = optional_param('id', array(), PARAM_INT);
 700   *
 701   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 702   *
 703   * @param string $parname the name of the page parameter we want
 704   * @param mixed $default the default value to return if nothing is found
 705   * @param string $type expected type of parameter
 706   * @return array
 707   * @throws coding_exception
 708   */
 709  function optional_param_array($parname, $default, $type) {
 710      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 711          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 712      }
 713  
 714      // POST has precedence.
 715      if (isset($_POST[$parname])) {
 716          $param = $_POST[$parname];
 717      } else if (isset($_GET[$parname])) {
 718          $param = $_GET[$parname];
 719      } else {
 720          return $default;
 721      }
 722      if (!is_array($param)) {
 723          debugging('optional_param_array() expects array parameters only: '.$parname);
 724          return $default;
 725      }
 726  
 727      $result = array();
 728      foreach ($param as $key => $value) {
 729          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 730              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
 731              continue;
 732          }
 733          $result[$key] = clean_param($value, $type);
 734      }
 735  
 736      return $result;
 737  }
 738  
 739  /**
 740   * Strict validation of parameter values, the values are only converted
 741   * to requested PHP type. Internally it is using clean_param, the values
 742   * before and after cleaning must be equal - otherwise
 743   * an invalid_parameter_exception is thrown.
 744   * Objects and classes are not accepted.
 745   *
 746   * @param mixed $param
 747   * @param string $type PARAM_ constant
 748   * @param bool $allownull are nulls valid value?
 749   * @param string $debuginfo optional debug information
 750   * @return mixed the $param value converted to PHP type
 751   * @throws invalid_parameter_exception if $param is not of given type
 752   */
 753  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
 754      if (is_null($param)) {
 755          if ($allownull == NULL_ALLOWED) {
 756              return null;
 757          } else {
 758              throw new invalid_parameter_exception($debuginfo);
 759          }
 760      }
 761      if (is_array($param) or is_object($param)) {
 762          throw new invalid_parameter_exception($debuginfo);
 763      }
 764  
 765      $cleaned = clean_param($param, $type);
 766  
 767      if ($type == PARAM_FLOAT) {
 768          // Do not detect precision loss here.
 769          if (is_float($param) or is_int($param)) {
 770              // These always fit.
 771          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
 772              throw new invalid_parameter_exception($debuginfo);
 773          }
 774      } else if ((string)$param !== (string)$cleaned) {
 775          // Conversion to string is usually lossless.
 776          throw new invalid_parameter_exception($debuginfo);
 777      }
 778  
 779      return $cleaned;
 780  }
 781  
 782  /**
 783   * Makes sure array contains only the allowed types, this function does not validate array key names!
 784   *
 785   * <code>
 786   * $options = clean_param($options, PARAM_INT);
 787   * </code>
 788   *
 789   * @param array|null $param the variable array we are cleaning
 790   * @param string $type expected format of param after cleaning.
 791   * @param bool $recursive clean recursive arrays
 792   * @return array
 793   * @throws coding_exception
 794   */
 795  function clean_param_array(?array $param, $type, $recursive = false) {
 796      // Convert null to empty array.
 797      $param = (array)$param;
 798      foreach ($param as $key => $value) {
 799          if (is_array($value)) {
 800              if ($recursive) {
 801                  $param[$key] = clean_param_array($value, $type, true);
 802              } else {
 803                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
 804              }
 805          } else {
 806              $param[$key] = clean_param($value, $type);
 807          }
 808      }
 809      return $param;
 810  }
 811  
 812  /**
 813   * Used by {@link optional_param()} and {@link required_param()} to
 814   * clean the variables and/or cast to specific types, based on
 815   * an options field.
 816   * <code>
 817   * $course->format = clean_param($course->format, PARAM_ALPHA);
 818   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
 819   * </code>
 820   *
 821   * @param mixed $param the variable we are cleaning
 822   * @param string $type expected format of param after cleaning.
 823   * @return mixed
 824   * @throws coding_exception
 825   */
 826  function clean_param($param, $type) {
 827      global $CFG;
 828  
 829      if (is_array($param)) {
 830          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
 831      } else if (is_object($param)) {
 832          if (method_exists($param, '__toString')) {
 833              $param = $param->__toString();
 834          } else {
 835              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
 836          }
 837      }
 838  
 839      switch ($type) {
 840          case PARAM_RAW:
 841              // No cleaning at all.
 842              $param = fix_utf8($param);
 843              return $param;
 844  
 845          case PARAM_RAW_TRIMMED:
 846              // No cleaning, but strip leading and trailing whitespace.
 847              $param = fix_utf8($param);
 848              return trim($param);
 849  
 850          case PARAM_CLEAN:
 851              // General HTML cleaning, try to use more specific type if possible this is deprecated!
 852              // Please use more specific type instead.
 853              if (is_numeric($param)) {
 854                  return $param;
 855              }
 856              $param = fix_utf8($param);
 857              // Sweep for scripts, etc.
 858              return clean_text($param);
 859  
 860          case PARAM_CLEANHTML:
 861              // Clean html fragment.
 862              $param = fix_utf8($param);
 863              // Sweep for scripts, etc.
 864              $param = clean_text($param, FORMAT_HTML);
 865              return trim($param);
 866  
 867          case PARAM_INT:
 868              // Convert to integer.
 869              return (int)$param;
 870  
 871          case PARAM_FLOAT:
 872              // Convert to float.
 873              return (float)$param;
 874  
 875          case PARAM_LOCALISEDFLOAT:
 876              // Convert to float.
 877              return unformat_float($param, true);
 878  
 879          case PARAM_ALPHA:
 880              // Remove everything not `a-z`.
 881              return preg_replace('/[^a-zA-Z]/i', '', $param);
 882  
 883          case PARAM_ALPHAEXT:
 884              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
 885              return preg_replace('/[^a-zA-Z_-]/i', '', $param);
 886  
 887          case PARAM_ALPHANUM:
 888              // Remove everything not `a-zA-Z0-9`.
 889              return preg_replace('/[^A-Za-z0-9]/i', '', $param);
 890  
 891          case PARAM_ALPHANUMEXT:
 892              // Remove everything not `a-zA-Z0-9_-`.
 893              return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
 894  
 895          case PARAM_SEQUENCE:
 896              // Remove everything not `0-9,`.
 897              return preg_replace('/[^0-9,]/i', '', $param);
 898  
 899          case PARAM_BOOL:
 900              // Convert to 1 or 0.
 901              $tempstr = strtolower($param);
 902              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
 903                  $param = 1;
 904              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
 905                  $param = 0;
 906              } else {
 907                  $param = empty($param) ? 0 : 1;
 908              }
 909              return $param;
 910  
 911          case PARAM_NOTAGS:
 912              // Strip all tags.
 913              $param = fix_utf8($param);
 914              return strip_tags($param);
 915  
 916          case PARAM_TEXT:
 917              // Leave only tags needed for multilang.
 918              $param = fix_utf8($param);
 919              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
 920              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
 921              do {
 922                  if (strpos($param, '</lang>') !== false) {
 923                      // Old and future mutilang syntax.
 924                      $param = strip_tags($param, '<lang>');
 925                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 926                          break;
 927                      }
 928                      $open = false;
 929                      foreach ($matches[0] as $match) {
 930                          if ($match === '</lang>') {
 931                              if ($open) {
 932                                  $open = false;
 933                                  continue;
 934                              } else {
 935                                  break 2;
 936                              }
 937                          }
 938                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
 939                              break 2;
 940                          } else {
 941                              $open = true;
 942                          }
 943                      }
 944                      if ($open) {
 945                          break;
 946                      }
 947                      return $param;
 948  
 949                  } else if (strpos($param, '</span>') !== false) {
 950                      // Current problematic multilang syntax.
 951                      $param = strip_tags($param, '<span>');
 952                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 953                          break;
 954                      }
 955                      $open = false;
 956                      foreach ($matches[0] as $match) {
 957                          if ($match === '</span>') {
 958                              if ($open) {
 959                                  $open = false;
 960                                  continue;
 961                              } else {
 962                                  break 2;
 963                              }
 964                          }
 965                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
 966                              break 2;
 967                          } else {
 968                              $open = true;
 969                          }
 970                      }
 971                      if ($open) {
 972                          break;
 973                      }
 974                      return $param;
 975                  }
 976              } while (false);
 977              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
 978              return strip_tags($param);
 979  
 980          case PARAM_COMPONENT:
 981              // We do not want any guessing here, either the name is correct or not
 982              // please note only normalised component names are accepted.
 983              if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
 984                  return '';
 985              }
 986              if (strpos($param, '__') !== false) {
 987                  return '';
 988              }
 989              if (strpos($param, 'mod_') === 0) {
 990                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
 991                  if (substr_count($param, '_') != 1) {
 992                      return '';
 993                  }
 994              }
 995              return $param;
 996  
 997          case PARAM_PLUGIN:
 998          case PARAM_AREA:
 999              // We do not want any guessing here, either the name is correct or not.
1000              if (!is_valid_plugin_name($param)) {
1001                  return '';
1002              }
1003              return $param;
1004  
1005          case PARAM_SAFEDIR:
1006              // Remove everything not a-zA-Z0-9_- .
1007              return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
1008  
1009          case PARAM_SAFEPATH:
1010              // Remove everything not a-zA-Z0-9/_- .
1011              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
1012  
1013          case PARAM_FILE:
1014              // Strip all suspicious characters from filename.
1015              $param = fix_utf8($param);
1016              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
1017              if ($param === '.' || $param === '..') {
1018                  $param = '';
1019              }
1020              return $param;
1021  
1022          case PARAM_PATH:
1023              // Strip all suspicious characters from file path.
1024              $param = fix_utf8($param);
1025              $param = str_replace('\\', '/', $param);
1026  
1027              // Explode the path and clean each element using the PARAM_FILE rules.
1028              $breadcrumb = explode('/', $param);
1029              foreach ($breadcrumb as $key => $crumb) {
1030                  if ($crumb === '.' && $key === 0) {
1031                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
1032                  } else {
1033                      $crumb = clean_param($crumb, PARAM_FILE);
1034                  }
1035                  $breadcrumb[$key] = $crumb;
1036              }
1037              $param = implode('/', $breadcrumb);
1038  
1039              // Remove multiple current path (./././) and multiple slashes (///).
1040              $param = preg_replace('~//+~', '/', $param);
1041              $param = preg_replace('~/(\./)+~', '/', $param);
1042              return $param;
1043  
1044          case PARAM_HOST:
1045              // Allow FQDN or IPv4 dotted quad.
1046              $param = preg_replace('/[^\.\d\w-]/', '', $param );
1047              // Match ipv4 dotted quad.
1048              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1049                  // Confirm values are ok.
1050                  if ( $match[0] > 255
1051                       || $match[1] > 255
1052                       || $match[3] > 255
1053                       || $match[4] > 255 ) {
1054                      // Hmmm, what kind of dotted quad is this?
1055                      $param = '';
1056                  }
1057              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1058                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1059                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1060                         ) {
1061                  // All is ok - $param is respected.
1062              } else {
1063                  // All is not ok...
1064                  $param='';
1065              }
1066              return $param;
1067  
1068          case PARAM_URL:
1069              // Allow safe urls.
1070              $param = fix_utf8($param);
1071              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1072              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1073                  // All is ok, param is respected.
1074              } else {
1075                  // Not really ok.
1076                  $param ='';
1077              }
1078              return $param;
1079  
1080          case PARAM_LOCALURL:
1081              // Allow http absolute, root relative and relative URLs within wwwroot.
1082              $param = clean_param($param, PARAM_URL);
1083              if (!empty($param)) {
1084  
1085                  if ($param === $CFG->wwwroot) {
1086                      // Exact match;
1087                  } else if (preg_match(':^/:', $param)) {
1088                      // Root-relative, ok!
1089                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1090                      // Absolute, and matches our wwwroot.
1091                  } else {
1092  
1093                      // Relative - let's make sure there are no tricks.
1094                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
1095                          // Looks ok.
1096                      } else {
1097                          $param = '';
1098                      }
1099                  }
1100              }
1101              return $param;
1102  
1103          case PARAM_PEM:
1104              $param = trim($param);
1105              // PEM formatted strings may contain letters/numbers and the symbols:
1106              //   forward slash: /
1107              //   plus sign:     +
1108              //   equal sign:    =
1109              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1110              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1111                  list($wholething, $body) = $matches;
1112                  unset($wholething, $matches);
1113                  $b64 = clean_param($body, PARAM_BASE64);
1114                  if (!empty($b64)) {
1115                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1116                  } else {
1117                      return '';
1118                  }
1119              }
1120              return '';
1121  
1122          case PARAM_BASE64:
1123              if (!empty($param)) {
1124                  // PEM formatted strings may contain letters/numbers and the symbols
1125                  //   forward slash: /
1126                  //   plus sign:     +
1127                  //   equal sign:    =.
1128                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1129                      return '';
1130                  }
1131                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1132                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1133                  // than (or equal to) 64 characters long.
1134                  for ($i=0, $j=count($lines); $i < $j; $i++) {
1135                      if ($i + 1 == $j) {
1136                          if (64 < strlen($lines[$i])) {
1137                              return '';
1138                          }
1139                          continue;
1140                      }
1141  
1142                      if (64 != strlen($lines[$i])) {
1143                          return '';
1144                      }
1145                  }
1146                  return implode("\n", $lines);
1147              } else {
1148                  return '';
1149              }
1150  
1151          case PARAM_TAG:
1152              $param = fix_utf8($param);
1153              // Please note it is not safe to use the tag name directly anywhere,
1154              // it must be processed with s(), urlencode() before embedding anywhere.
1155              // Remove some nasties.
1156              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1157              // Convert many whitespace chars into one.
1158              $param = preg_replace('/\s+/u', ' ', $param);
1159              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1160              return $param;
1161  
1162          case PARAM_TAGLIST:
1163              $param = fix_utf8($param);
1164              $tags = explode(',', $param);
1165              $result = array();
1166              foreach ($tags as $tag) {
1167                  $res = clean_param($tag, PARAM_TAG);
1168                  if ($res !== '') {
1169                      $result[] = $res;
1170                  }
1171              }
1172              if ($result) {
1173                  return implode(',', $result);
1174              } else {
1175                  return '';
1176              }
1177  
1178          case PARAM_CAPABILITY:
1179              if (get_capability_info($param)) {
1180                  return $param;
1181              } else {
1182                  return '';
1183              }
1184  
1185          case PARAM_PERMISSION:
1186              $param = (int)$param;
1187              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1188                  return $param;
1189              } else {
1190                  return CAP_INHERIT;
1191              }
1192  
1193          case PARAM_AUTH:
1194              $param = clean_param($param, PARAM_PLUGIN);
1195              if (empty($param)) {
1196                  return '';
1197              } else if (exists_auth_plugin($param)) {
1198                  return $param;
1199              } else {
1200                  return '';
1201              }
1202  
1203          case PARAM_LANG:
1204              $param = clean_param($param, PARAM_SAFEDIR);
1205              if (get_string_manager()->translation_exists($param)) {
1206                  return $param;
1207              } else {
1208                  // Specified language is not installed or param malformed.
1209                  return '';
1210              }
1211  
1212          case PARAM_THEME:
1213              $param = clean_param($param, PARAM_PLUGIN);
1214              if (empty($param)) {
1215                  return '';
1216              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1217                  return $param;
1218              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1219                  return $param;
1220              } else {
1221                  // Specified theme is not installed.
1222                  return '';
1223              }
1224  
1225          case PARAM_USERNAME:
1226              $param = fix_utf8($param);
1227              $param = trim($param);
1228              // Convert uppercase to lowercase MDL-16919.
1229              $param = core_text::strtolower($param);
1230              if (empty($CFG->extendedusernamechars)) {
1231                  $param = str_replace(" " , "", $param);
1232                  // Regular expression, eliminate all chars EXCEPT:
1233                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1234                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1235              }
1236              return $param;
1237  
1238          case PARAM_EMAIL:
1239              $param = fix_utf8($param);
1240              if (validate_email($param)) {
1241                  return $param;
1242              } else {
1243                  return '';
1244              }
1245  
1246          case PARAM_STRINGID:
1247              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1248                  return $param;
1249              } else {
1250                  return '';
1251              }
1252  
1253          case PARAM_TIMEZONE:
1254              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1255              $param = fix_utf8($param);
1256              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1257              if (preg_match($timezonepattern, $param)) {
1258                  return $param;
1259              } else {
1260                  return '';
1261              }
1262  
1263          default:
1264              // Doh! throw error, switched parameters in optional_param or another serious problem.
1265              print_error("unknownparamtype", '', '', $type);
1266      }
1267  }
1268  
1269  /**
1270   * Whether the PARAM_* type is compatible in RTL.
1271   *
1272   * Being compatible with RTL means that the data they contain can flow
1273   * from right-to-left or left-to-right without compromising the user experience.
1274   *
1275   * Take URLs for example, they are not RTL compatible as they should always
1276   * flow from the left to the right. This also applies to numbers, email addresses,
1277   * configuration snippets, base64 strings, etc...
1278   *
1279   * This function tries to best guess which parameters can contain localised strings.
1280   *
1281   * @param string $paramtype Constant PARAM_*.
1282   * @return bool
1283   */
1284  function is_rtl_compatible($paramtype) {
1285      return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1286  }
1287  
1288  /**
1289   * Makes sure the data is using valid utf8, invalid characters are discarded.
1290   *
1291   * Note: this function is not intended for full objects with methods and private properties.
1292   *
1293   * @param mixed $value
1294   * @return mixed with proper utf-8 encoding
1295   */
1296  function fix_utf8($value) {
1297      if (is_null($value) or $value === '') {
1298          return $value;
1299  
1300      } else if (is_string($value)) {
1301          if ((string)(int)$value === $value) {
1302              // Shortcut.
1303              return $value;
1304          }
1305  
1306          // Remove null bytes or invalid Unicode sequences from value.
1307          $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
1308  
1309          // Note: this duplicates min_fix_utf8() intentionally.
1310          static $buggyiconv = null;
1311          if ($buggyiconv === null) {
1312              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1313          }
1314  
1315          if ($buggyiconv) {
1316              if (function_exists('mb_convert_encoding')) {
1317                  $subst = mb_substitute_character();
1318                  mb_substitute_character('none');
1319                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1320                  mb_substitute_character($subst);
1321  
1322              } else {
1323                  // Warn admins on admin/index.php page.
1324                  $result = $value;
1325              }
1326  
1327          } else {
1328              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1329          }
1330  
1331          return $result;
1332  
1333      } else if (is_array($value)) {
1334          foreach ($value as $k => $v) {
1335              $value[$k] = fix_utf8($v);
1336          }
1337          return $value;
1338  
1339      } else if (is_object($value)) {
1340          // Do not modify original.
1341          $value = clone($value);
1342          foreach ($value as $k => $v) {
1343              $value->$k = fix_utf8($v);
1344          }
1345          return $value;
1346  
1347      } else {
1348          // This is some other type, no utf-8 here.
1349          return $value;
1350      }
1351  }
1352  
1353  /**
1354   * Return true if given value is integer or string with integer value
1355   *
1356   * @param mixed $value String or Int
1357   * @return bool true if number, false if not
1358   */
1359  function is_number($value) {
1360      if (is_int($value)) {
1361          return true;
1362      } else if (is_string($value)) {
1363          return ((string)(int)$value) === $value;
1364      } else {
1365          return false;
1366      }
1367  }
1368  
1369  /**
1370   * Returns host part from url.
1371   *
1372   * @param string $url full url
1373   * @return string host, null if not found
1374   */
1375  function get_host_from_url($url) {
1376      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1377      if ($matches) {
1378          return $matches[1];
1379      }
1380      return null;
1381  }
1382  
1383  /**
1384   * Tests whether anything was returned by text editor
1385   *
1386   * This function is useful for testing whether something you got back from
1387   * the HTML editor actually contains anything. Sometimes the HTML editor
1388   * appear to be empty, but actually you get back a <br> tag or something.
1389   *
1390   * @param string $string a string containing HTML.
1391   * @return boolean does the string contain any actual content - that is text,
1392   * images, objects, etc.
1393   */
1394  function html_is_blank($string) {
1395      return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1396  }
1397  
1398  /**
1399   * Set a key in global configuration
1400   *
1401   * Set a key/value pair in both this session's {@link $CFG} global variable
1402   * and in the 'config' database table for future sessions.
1403   *
1404   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1405   * In that case it doesn't affect $CFG.
1406   *
1407   * A NULL value will delete the entry.
1408   *
1409   * NOTE: this function is called from lib/db/upgrade.php
1410   *
1411   * @param string $name the key to set
1412   * @param string $value the value to set (without magic quotes)
1413   * @param string $plugin (optional) the plugin scope, default null
1414   * @return bool true or exception
1415   */
1416  function set_config($name, $value, $plugin=null) {
1417      global $CFG, $DB;
1418  
1419      if (empty($plugin)) {
1420          if (!array_key_exists($name, $CFG->config_php_settings)) {
1421              // So it's defined for this invocation at least.
1422              if (is_null($value)) {
1423                  unset($CFG->$name);
1424              } else {
1425                  // Settings from db are always strings.
1426                  $CFG->$name = (string)$value;
1427              }
1428          }
1429  
1430          if ($DB->get_field('config', 'name', array('name' => $name))) {
1431              if ($value === null) {
1432                  $DB->delete_records('config', array('name' => $name));
1433              } else {
1434                  $DB->set_field('config', 'value', $value, array('name' => $name));
1435              }
1436          } else {
1437              if ($value !== null) {
1438                  $config = new stdClass();
1439                  $config->name  = $name;
1440                  $config->value = $value;
1441                  $DB->insert_record('config', $config, false);
1442              }
1443              // When setting config during a Behat test (in the CLI script, not in the web browser
1444              // requests), remember which ones are set so that we can clear them later.
1445              if (defined('BEHAT_TEST')) {
1446                  if (!property_exists($CFG, 'behat_cli_added_config')) {
1447                      $CFG->behat_cli_added_config = [];
1448                  }
1449                  $CFG->behat_cli_added_config[$name] = true;
1450              }
1451          }
1452          if ($name === 'siteidentifier') {
1453              cache_helper::update_site_identifier($value);
1454          }
1455          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1456      } else {
1457          // Plugin scope.
1458          if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1459              if ($value===null) {
1460                  $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1461              } else {
1462                  $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1463              }
1464          } else {
1465              if ($value !== null) {
1466                  $config = new stdClass();
1467                  $config->plugin = $plugin;
1468                  $config->name   = $name;
1469                  $config->value  = $value;
1470                  $DB->insert_record('config_plugins', $config, false);
1471              }
1472          }
1473          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1474      }
1475  
1476      return true;
1477  }
1478  
1479  /**
1480   * Get configuration values from the global config table
1481   * or the config_plugins table.
1482   *
1483   * If called with one parameter, it will load all the config
1484   * variables for one plugin, and return them as an object.
1485   *
1486   * If called with 2 parameters it will return a string single
1487   * value or false if the value is not found.
1488   *
1489   * NOTE: this function is called from lib/db/upgrade.php
1490   *
1491   * @param string $plugin full component name
1492   * @param string $name default null
1493   * @return mixed hash-like object or single value, return false no config found
1494   * @throws dml_exception
1495   */
1496  function get_config($plugin, $name = null) {
1497      global $CFG, $DB;
1498  
1499      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1500          $forced =& $CFG->config_php_settings;
1501          $iscore = true;
1502          $plugin = 'core';
1503      } else {
1504          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1505              $forced =& $CFG->forced_plugin_settings[$plugin];
1506          } else {
1507              $forced = array();
1508          }
1509          $iscore = false;
1510      }
1511  
1512      if (!isset($CFG->siteidentifier)) {
1513          try {
1514              // This may throw an exception during installation, which is how we detect the
1515              // need to install the database. For more details see {@see initialise_cfg()}.
1516              $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1517          } catch (dml_exception $ex) {
1518              // Set siteidentifier to false. We don't want to trip this continually.
1519              $siteidentifier = false;
1520              throw $ex;
1521          }
1522      }
1523  
1524      if (!empty($name)) {
1525          if (array_key_exists($name, $forced)) {
1526              return (string)$forced[$name];
1527          } else if ($name === 'siteidentifier' && $plugin == 'core') {
1528              return $CFG->siteidentifier;
1529          }
1530      }
1531  
1532      $cache = cache::make('core', 'config');
1533      $result = $cache->get($plugin);
1534      if ($result === false) {
1535          // The user is after a recordset.
1536          if (!$iscore) {
1537              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1538          } else {
1539              // This part is not really used any more, but anyway...
1540              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1541          }
1542          $cache->set($plugin, $result);
1543      }
1544  
1545      if (!empty($name)) {
1546          if (array_key_exists($name, $result)) {
1547              return $result[$name];
1548          }
1549          return false;
1550      }
1551  
1552      if ($plugin === 'core') {
1553          $result['siteidentifier'] = $CFG->siteidentifier;
1554      }
1555  
1556      foreach ($forced as $key => $value) {
1557          if (is_null($value) or is_array($value) or is_object($value)) {
1558              // We do not want any extra mess here, just real settings that could be saved in db.
1559              unset($result[$key]);
1560          } else {
1561              // Convert to string as if it went through the DB.
1562              $result[$key] = (string)$value;
1563          }
1564      }
1565  
1566      return (object)$result;
1567  }
1568  
1569  /**
1570   * Removes a key from global configuration.
1571   *
1572   * NOTE: this function is called from lib/db/upgrade.php
1573   *
1574   * @param string $name the key to set
1575   * @param string $plugin (optional) the plugin scope
1576   * @return boolean whether the operation succeeded.
1577   */
1578  function unset_config($name, $plugin=null) {
1579      global $CFG, $DB;
1580  
1581      if (empty($plugin)) {
1582          unset($CFG->$name);
1583          $DB->delete_records('config', array('name' => $name));
1584          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1585      } else {
1586          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1587          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1588      }
1589  
1590      return true;
1591  }
1592  
1593  /**
1594   * Remove all the config variables for a given plugin.
1595   *
1596   * NOTE: this function is called from lib/db/upgrade.php
1597   *
1598   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1599   * @return boolean whether the operation succeeded.
1600   */
1601  function unset_all_config_for_plugin($plugin) {
1602      global $DB;
1603      // Delete from the obvious config_plugins first.
1604      $DB->delete_records('config_plugins', array('plugin' => $plugin));
1605      // Next delete any suspect settings from config.
1606      $like = $DB->sql_like('name', '?', true, true, false, '|');
1607      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1608      $DB->delete_records_select('config', $like, $params);
1609      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1610      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1611  
1612      return true;
1613  }
1614  
1615  /**
1616   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1617   *
1618   * All users are verified if they still have the necessary capability.
1619   *
1620   * @param string $value the value of the config setting.
1621   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1622   * @param bool $includeadmins include administrators.
1623   * @return array of user objects.
1624   */
1625  function get_users_from_config($value, $capability, $includeadmins = true) {
1626      if (empty($value) or $value === '$@NONE@$') {
1627          return array();
1628      }
1629  
1630      // We have to make sure that users still have the necessary capability,
1631      // it should be faster to fetch them all first and then test if they are present
1632      // instead of validating them one-by-one.
1633      $users = get_users_by_capability(context_system::instance(), $capability);
1634      if ($includeadmins) {
1635          $admins = get_admins();
1636          foreach ($admins as $admin) {
1637              $users[$admin->id] = $admin;
1638          }
1639      }
1640  
1641      if ($value === '$@ALL@$') {
1642          return $users;
1643      }
1644  
1645      $result = array(); // Result in correct order.
1646      $allowed = explode(',', $value);
1647      foreach ($allowed as $uid) {
1648          if (isset($users[$uid])) {
1649              $user = $users[$uid];
1650              $result[$user->id] = $user;
1651          }
1652      }
1653  
1654      return $result;
1655  }
1656  
1657  
1658  /**
1659   * Invalidates browser caches and cached data in temp.
1660   *
1661   * @return void
1662   */
1663  function purge_all_caches() {
1664      purge_caches();
1665  }
1666  
1667  /**
1668   * Selectively invalidate different types of cache.
1669   *
1670   * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1671   * areas alone or in combination.
1672   *
1673   * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1674   *        'muc'    Purge MUC caches?
1675   *        'theme'  Purge theme cache?
1676   *        'lang'   Purge language string cache?
1677   *        'js'     Purge javascript cache?
1678   *        'filter' Purge text filter cache?
1679   *        'other'  Purge all other caches?
1680   */
1681  function purge_caches($options = []) {
1682      $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1683      if (empty(array_filter($options))) {
1684          $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1685      } else {
1686          $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1687      }
1688      if ($options['muc']) {
1689          cache_helper::purge_all();
1690      }
1691      if ($options['theme']) {
1692          theme_reset_all_caches();
1693      }
1694      if ($options['lang']) {
1695          get_string_manager()->reset_caches();
1696      }
1697      if ($options['js']) {
1698          js_reset_all_caches();
1699      }
1700      if ($options['template']) {
1701          template_reset_all_caches();
1702      }
1703      if ($options['filter']) {
1704          reset_text_filters_cache();
1705      }
1706      if ($options['other']) {
1707          purge_other_caches();
1708      }
1709  }
1710  
1711  /**
1712   * Purge all non-MUC caches not otherwise purged in purge_caches.
1713   *
1714   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1715   * {@link phpunit_util::reset_dataroot()}
1716   */
1717  function purge_other_caches() {
1718      global $DB, $CFG;
1719      if (class_exists('core_plugin_manager')) {
1720          core_plugin_manager::reset_caches();
1721      }
1722  
1723      // Bump up cacherev field for all courses.
1724      try {
1725          increment_revision_number('course', 'cacherev', '');
1726      } catch (moodle_exception $e) {
1727          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1728      }
1729  
1730      $DB->reset_caches();
1731  
1732      // Purge all other caches: rss, simplepie, etc.
1733      clearstatcache();
1734      remove_dir($CFG->cachedir.'', true);
1735  
1736      // Make sure cache dir is writable, throws exception if not.
1737      make_cache_directory('');
1738  
1739      // This is the only place where we purge local caches, we are only adding files there.
1740      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1741      remove_dir($CFG->localcachedir, true);
1742      set_config('localcachedirpurged', time());
1743      make_localcache_directory('', true);
1744      \core\task\manager::clear_static_caches();
1745  }
1746  
1747  /**
1748   * Get volatile flags
1749   *
1750   * @param string $type
1751   * @param int $changedsince default null
1752   * @return array records array
1753   */
1754  function get_cache_flags($type, $changedsince = null) {
1755      global $DB;
1756  
1757      $params = array('type' => $type, 'expiry' => time());
1758      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1759      if ($changedsince !== null) {
1760          $params['changedsince'] = $changedsince;
1761          $sqlwhere .= " AND timemodified > :changedsince";
1762      }
1763      $cf = array();
1764      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1765          foreach ($flags as $flag) {
1766              $cf[$flag->name] = $flag->value;
1767          }
1768      }
1769      return $cf;
1770  }
1771  
1772  /**
1773   * Get volatile flags
1774   *
1775   * @param string $type
1776   * @param string $name
1777   * @param int $changedsince default null
1778   * @return string|false The cache flag value or false
1779   */
1780  function get_cache_flag($type, $name, $changedsince=null) {
1781      global $DB;
1782  
1783      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1784  
1785      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1786      if ($changedsince !== null) {
1787          $params['changedsince'] = $changedsince;
1788          $sqlwhere .= " AND timemodified > :changedsince";
1789      }
1790  
1791      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1792  }
1793  
1794  /**
1795   * Set a volatile flag
1796   *
1797   * @param string $type the "type" namespace for the key
1798   * @param string $name the key to set
1799   * @param string $value the value to set (without magic quotes) - null will remove the flag
1800   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1801   * @return bool Always returns true
1802   */
1803  function set_cache_flag($type, $name, $value, $expiry = null) {
1804      global $DB;
1805  
1806      $timemodified = time();
1807      if ($expiry === null || $expiry < $timemodified) {
1808          $expiry = $timemodified + 24 * 60 * 60;
1809      } else {
1810          $expiry = (int)$expiry;
1811      }
1812  
1813      if ($value === null) {
1814          unset_cache_flag($type, $name);
1815          return true;
1816      }
1817  
1818      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1819          // This is a potential problem in DEBUG_DEVELOPER.
1820          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1821              return true; // No need to update.
1822          }
1823          $f->value        = $value;
1824          $f->expiry       = $expiry;
1825          $f->timemodified = $timemodified;
1826          $DB->update_record('cache_flags', $f);
1827      } else {
1828          $f = new stdClass();
1829          $f->flagtype     = $type;
1830          $f->name         = $name;
1831          $f->value        = $value;
1832          $f->expiry       = $expiry;
1833          $f->timemodified = $timemodified;
1834          $DB->insert_record('cache_flags', $f);
1835      }
1836      return true;
1837  }
1838  
1839  /**
1840   * Removes a single volatile flag
1841   *
1842   * @param string $type the "type" namespace for the key
1843   * @param string $name the key to set
1844   * @return bool
1845   */
1846  function unset_cache_flag($type, $name) {
1847      global $DB;
1848      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1849      return true;
1850  }
1851  
1852  /**
1853   * Garbage-collect volatile flags
1854   *
1855   * @return bool Always returns true
1856   */
1857  function gc_cache_flags() {
1858      global $DB;
1859      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1860      return true;
1861  }
1862  
1863  // USER PREFERENCE API.
1864  
1865  /**
1866   * Refresh user preference cache. This is used most often for $USER
1867   * object that is stored in session, but it also helps with performance in cron script.
1868   *
1869   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1870   *
1871   * @package  core
1872   * @category preference
1873   * @access   public
1874   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1875   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1876   * @throws   coding_exception
1877   * @return   null
1878   */
1879  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1880      global $DB;
1881      // Static cache, we need to check on each page load, not only every 2 minutes.
1882      static $loadedusers = array();
1883  
1884      if (!isset($user->id)) {
1885          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1886      }
1887  
1888      if (empty($user->id) or isguestuser($user->id)) {
1889          // No permanent storage for not-logged-in users and guest.
1890          if (!isset($user->preference)) {
1891              $user->preference = array();
1892          }
1893          return;
1894      }
1895  
1896      $timenow = time();
1897  
1898      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1899          // Already loaded at least once on this page. Are we up to date?
1900          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1901              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1902              return;
1903  
1904          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1905              // No change since the lastcheck on this page.
1906              $user->preference['_lastloaded'] = $timenow;
1907              return;
1908          }
1909      }
1910  
1911      // OK, so we have to reload all preferences.
1912      $loadedusers[$user->id] = true;
1913      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1914      $user->preference['_lastloaded'] = $timenow;
1915  }
1916  
1917  /**
1918   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1919   *
1920   * NOTE: internal function, do not call from other code.
1921   *
1922   * @package core
1923   * @access private
1924   * @param integer $userid the user whose prefs were changed.
1925   */
1926  function mark_user_preferences_changed($userid) {
1927      global $CFG;
1928  
1929      if (empty($userid) or isguestuser($userid)) {
1930          // No cache flags for guest and not-logged-in users.
1931          return;
1932      }
1933  
1934      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1935  }
1936  
1937  /**
1938   * Sets a preference for the specified user.
1939   *
1940   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1941   *
1942   * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1943   *
1944   * @package  core
1945   * @category preference
1946   * @access   public
1947   * @param    string            $name  The key to set as preference for the specified user
1948   * @param    string            $value The value to set for the $name key in the specified user's
1949   *                                    record, null means delete current value.
1950   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1951   * @throws   coding_exception
1952   * @return   bool                     Always true or exception
1953   */
1954  function set_user_preference($name, $value, $user = null) {
1955      global $USER, $DB;
1956  
1957      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1958          throw new coding_exception('Invalid preference name in set_user_preference() call');
1959      }
1960  
1961      if (is_null($value)) {
1962          // Null means delete current.
1963          return unset_user_preference($name, $user);
1964      } else if (is_object($value)) {
1965          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1966      } else if (is_array($value)) {
1967          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1968      }
1969      // Value column maximum length is 1333 characters.
1970      $value = (string)$value;
1971      if (core_text::strlen($value) > 1333) {
1972          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1973      }
1974  
1975      if (is_null($user)) {
1976          $user = $USER;
1977      } else if (isset($user->id)) {
1978          // It is a valid object.
1979      } else if (is_numeric($user)) {
1980          $user = (object)array('id' => (int)$user);
1981      } else {
1982          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1983      }
1984  
1985      check_user_preferences_loaded($user);
1986  
1987      if (empty($user->id) or isguestuser($user->id)) {
1988          // No permanent storage for not-logged-in users and guest.
1989          $user->preference[$name] = $value;
1990          return true;
1991      }
1992  
1993      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1994          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1995              // Preference already set to this value.
1996              return true;
1997          }
1998          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1999  
2000      } else {
2001          $preference = new stdClass();
2002          $preference->userid = $user->id;
2003          $preference->name   = $name;
2004          $preference->value  = $value;
2005          $DB->insert_record('user_preferences', $preference);
2006      }
2007  
2008      // Update value in cache.
2009      $user->preference[$name] = $value;
2010      // Update the $USER in case where we've not a direct reference to $USER.
2011      if ($user !== $USER && $user->id == $USER->id) {
2012          $USER->preference[$name] = $value;
2013      }
2014  
2015      // Set reload flag for other sessions.
2016      mark_user_preferences_changed($user->id);
2017  
2018      return true;
2019  }
2020  
2021  /**
2022   * Sets a whole array of preferences for the current user
2023   *
2024   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2025   *
2026   * @package  core
2027   * @category preference
2028   * @access   public
2029   * @param    array             $prefarray An array of key/value pairs to be set
2030   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
2031   * @return   bool                         Always true or exception
2032   */
2033  function set_user_preferences(array $prefarray, $user = null) {
2034      foreach ($prefarray as $name => $value) {
2035          set_user_preference($name, $value, $user);
2036      }
2037      return true;
2038  }
2039  
2040  /**
2041   * Unsets a preference completely by deleting it from the database
2042   *
2043   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2044   *
2045   * @package  core
2046   * @category preference
2047   * @access   public
2048   * @param    string            $name The key to unset as preference for the specified user
2049   * @param    stdClass|int|null $user A moodle user object or id, null means current user
2050   * @throws   coding_exception
2051   * @return   bool                    Always true or exception
2052   */
2053  function unset_user_preference($name, $user = null) {
2054      global $USER, $DB;
2055  
2056      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2057          throw new coding_exception('Invalid preference name in unset_user_preference() call');
2058      }
2059  
2060      if (is_null($user)) {
2061          $user = $USER;
2062      } else if (isset($user->id)) {
2063          // It is a valid object.
2064      } else if (is_numeric($user)) {
2065          $user = (object)array('id' => (int)$user);
2066      } else {
2067          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2068      }
2069  
2070      check_user_preferences_loaded($user);
2071  
2072      if (empty($user->id) or isguestuser($user->id)) {
2073          // No permanent storage for not-logged-in user and guest.
2074          unset($user->preference[$name]);
2075          return true;
2076      }
2077  
2078      // Delete from DB.
2079      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2080  
2081      // Delete the preference from cache.
2082      unset($user->preference[$name]);
2083      // Update the $USER in case where we've not a direct reference to $USER.
2084      if ($user !== $USER && $user->id == $USER->id) {
2085          unset($USER->preference[$name]);
2086      }
2087  
2088      // Set reload flag for other sessions.
2089      mark_user_preferences_changed($user->id);
2090  
2091      return true;
2092  }
2093  
2094  /**
2095   * Used to fetch user preference(s)
2096   *
2097   * If no arguments are supplied this function will return
2098   * all of the current user preferences as an array.
2099   *
2100   * If a name is specified then this function
2101   * attempts to return that particular preference value.  If
2102   * none is found, then the optional value $default is returned,
2103   * otherwise null.
2104   *
2105   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2106   *
2107   * @package  core
2108   * @category preference
2109   * @access   public
2110   * @param    string            $name    Name of the key to use in finding a preference value
2111   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
2112   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
2113   * @throws   coding_exception
2114   * @return   string|mixed|null          A string containing the value of a single preference. An
2115   *                                      array with all of the preferences or null
2116   */
2117  function get_user_preferences($name = null, $default = null, $user = null) {
2118      global $USER;
2119  
2120      if (is_null($name)) {
2121          // All prefs.
2122      } else if (is_numeric($name) or $name === '_lastloaded') {
2123          throw new coding_exception('Invalid preference name in get_user_preferences() call');
2124      }
2125  
2126      if (is_null($user)) {
2127          $user = $USER;
2128      } else if (isset($user->id)) {
2129          // Is a valid object.
2130      } else if (is_numeric($user)) {
2131          if ($USER->id == $user) {
2132              $user = $USER;
2133          } else {
2134              $user = (object)array('id' => (int)$user);
2135          }
2136      } else {
2137          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2138      }
2139  
2140      check_user_preferences_loaded($user);
2141  
2142      if (empty($name)) {
2143          // All values.
2144          return $user->preference;
2145      } else if (isset($user->preference[$name])) {
2146          // The single string value.
2147          return $user->preference[$name];
2148      } else {
2149          // Default value (null if not specified).
2150          return $default;
2151      }
2152  }
2153  
2154  // FUNCTIONS FOR HANDLING TIME.
2155  
2156  /**
2157   * Given Gregorian date parts in user time produce a GMT timestamp.
2158   *
2159   * @package core
2160   * @category time
2161   * @param int $year The year part to create timestamp of
2162   * @param int $month The month part to create timestamp of
2163   * @param int $day The day part to create timestamp of
2164   * @param int $hour The hour part to create timestamp of
2165   * @param int $minute The minute part to create timestamp of
2166   * @param int $second The second part to create timestamp of
2167   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2168   *             if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2169   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2170   *             applied only if timezone is 99 or string.
2171   * @return int GMT timestamp
2172   */
2173  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2174      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2175      $date->setDate((int)$year, (int)$month, (int)$day);
2176      $date->setTime((int)$hour, (int)$minute, (int)$second);
2177  
2178      $time = $date->getTimestamp();
2179  
2180      if ($time === false) {
2181          throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2182              ' This can fail if year is more than 2038 and OS is 32 bit windows');
2183      }
2184  
2185      // Moodle BC DST stuff.
2186      if (!$applydst) {
2187          $time += dst_offset_on($time, $timezone);
2188      }
2189  
2190      return $time;
2191  
2192  }
2193  
2194  /**
2195   * Format a date/time (seconds) as weeks, days, hours etc as needed
2196   *
2197   * Given an amount of time in seconds, returns string
2198   * formatted nicely as years, days, hours etc as needed
2199   *
2200   * @package core
2201   * @category time
2202   * @uses MINSECS
2203   * @uses HOURSECS
2204   * @uses DAYSECS
2205   * @uses YEARSECS
2206   * @param int $totalsecs Time in seconds
2207   * @param stdClass $str Should be a time object
2208   * @return string A nicely formatted date/time string
2209   */
2210  function format_time($totalsecs, $str = null) {
2211  
2212      $totalsecs = abs($totalsecs);
2213  
2214      if (!$str) {
2215          // Create the str structure the slow way.
2216          $str = new stdClass();
2217          $str->day   = get_string('day');
2218          $str->days  = get_string('days');
2219          $str->hour  = get_string('hour');
2220          $str->hours = get_string('hours');
2221          $str->min   = get_string('min');
2222          $str->mins  = get_string('mins');
2223          $str->sec   = get_string('sec');
2224          $str->secs  = get_string('secs');
2225          $str->year  = get_string('year');
2226          $str->years = get_string('years');
2227      }
2228  
2229      $years     = floor($totalsecs/YEARSECS);
2230      $remainder = $totalsecs - ($years*YEARSECS);
2231      $days      = floor($remainder/DAYSECS);
2232      $remainder = $totalsecs - ($days*DAYSECS);
2233      $hours     = floor($remainder/HOURSECS);
2234      $remainder = $remainder - ($hours*HOURSECS);
2235      $mins      = floor($remainder/MINSECS);
2236      $secs      = $remainder - ($mins*MINSECS);
2237  
2238      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2239      $sm = ($mins == 1)  ? $str->min  : $str->mins;
2240      $sh = ($hours == 1) ? $str->hour : $str->hours;
2241      $sd = ($days == 1)  ? $str->day  : $str->days;
2242      $sy = ($years == 1)  ? $str->year  : $str->years;
2243  
2244      $oyears = '';
2245      $odays = '';
2246      $ohours = '';
2247      $omins = '';
2248      $osecs = '';
2249  
2250      if ($years) {
2251          $oyears  = $years .' '. $sy;
2252      }
2253      if ($days) {
2254          $odays  = $days .' '. $sd;
2255      }
2256      if ($hours) {
2257          $ohours = $hours .' '. $sh;
2258      }
2259      if ($mins) {
2260          $omins  = $mins .' '. $sm;
2261      }
2262      if ($secs) {
2263          $osecs  = $secs .' '. $ss;
2264      }
2265  
2266      if ($years) {
2267          return trim($oyears .' '. $odays);
2268      }
2269      if ($days) {
2270          return trim($odays .' '. $ohours);
2271      }
2272      if ($hours) {
2273          return trim($ohours .' '. $omins);
2274      }
2275      if ($mins) {
2276          return trim($omins .' '. $osecs);
2277      }
2278      if ($secs) {
2279          return $osecs;
2280      }
2281      return get_string('now');
2282  }
2283  
2284  /**
2285   * Returns a formatted string that represents a date in user time.
2286   *
2287   * @package core
2288   * @category time
2289   * @param int $date the timestamp in UTC, as obtained from the database.
2290   * @param string $format strftime format. You should probably get this using
2291   *        get_string('strftime...', 'langconfig');
2292   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2293   *        not 99 then daylight saving will not be added.
2294   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2295   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2296   *        If false then the leading zero is maintained.
2297   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2298   * @return string the formatted date/time.
2299   */
2300  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2301      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2302      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2303  }
2304  
2305  /**
2306   * Returns a html "time" tag with both the exact user date with timezone information
2307   * as a datetime attribute in the W3C format, and the user readable date and time as text.
2308   *
2309   * @package core
2310   * @category time
2311   * @param int $date the timestamp in UTC, as obtained from the database.
2312   * @param string $format strftime format. You should probably get this using
2313   *        get_string('strftime...', 'langconfig');
2314   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2315   *        not 99 then daylight saving will not be added.
2316   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2317   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2318   *        If false then the leading zero is maintained.
2319   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2320   * @return string the formatted date/time.
2321   */
2322  function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2323      $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2324      if (CLI_SCRIPT && !PHPUNIT_TEST) {
2325          return $userdatestr;
2326      }
2327      $machinedate = new DateTime();
2328      $machinedate->setTimestamp(intval($date));
2329      $machinedate->setTimezone(core_date::get_user_timezone_object());
2330  
2331      return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2332  }
2333  
2334  /**
2335   * Returns a formatted date ensuring it is UTF-8.
2336   *
2337   * If we are running under Windows convert to Windows encoding and then back to UTF-8
2338   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2339   *
2340   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2341   * @param string $format strftime format.
2342   * @param int|float|string $tz the user timezone
2343   * @return string the formatted date/time.
2344   * @since Moodle 2.3.3
2345   */
2346  function date_format_string($date, $format, $tz = 99) {
2347      global $CFG;
2348  
2349      $localewincharset = null;
2350      // Get the calendar type user is using.
2351      if ($CFG->ostype == 'WINDOWS') {
2352          $calendartype = \core_calendar\type_factory::get_calendar_instance();
2353          $localewincharset = $calendartype->locale_win_charset();
2354      }
2355  
2356      if ($localewincharset) {
2357          $format = core_text::convert($format, 'utf-8', $localewincharset);
2358      }
2359  
2360      date_default_timezone_set(core_date::get_user_timezone($tz));
2361  
2362      if (strftime('%p', 0) === strftime('%p', HOURSECS * 18)) {
2363          $datearray = getdate($date);
2364          $format = str_replace([
2365              '%P',
2366              '%p',
2367          ], [
2368              $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
2369              $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
2370          ], $format);
2371      }
2372  
2373      $datestring = strftime($format, $date);
2374      core_date::set_default_server_timezone();
2375  
2376      if ($localewincharset) {
2377          $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2378      }
2379  
2380      return $datestring;
2381  }
2382  
2383  /**
2384   * Given a $time timestamp in GMT (seconds since epoch),
2385   * returns an array that represents the Gregorian date in user time
2386   *
2387   * @package core
2388   * @category time
2389   * @param int $time Timestamp in GMT
2390   * @param float|int|string $timezone user timezone
2391   * @return array An array that represents the date in user time
2392   */
2393  function usergetdate($time, $timezone=99) {
2394      if ($time === null) {
2395          // PHP8 and PHP7 return different results when getdate(null) is called.
2396          // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
2397          // In the future versions of Moodle we may consider adding a strict typehint.
2398          debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
2399          $time = 0;
2400      }
2401  
2402      date_default_timezone_set(core_date::get_user_timezone($timezone));
2403      $result = getdate($time);
2404      core_date::set_default_server_timezone();
2405  
2406      return $result;
2407  }
2408  
2409  /**
2410   * Given a GMT timestamp (seconds since epoch), offsets it by
2411   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2412   *
2413   * NOTE: this function does not include DST properly,
2414   *       you should use the PHP date stuff instead!
2415   *
2416   * @package core
2417   * @category time
2418   * @param int $date Timestamp in GMT
2419   * @param float|int|string $timezone user timezone
2420   * @return int
2421   */
2422  function usertime($date, $timezone=99) {
2423      $userdate = new DateTime('@' . $date);
2424      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2425      $dst = dst_offset_on($date, $timezone);
2426  
2427      return $date - $userdate->getOffset() + $dst;
2428  }
2429  
2430  /**
2431   * Get a formatted string representation of an interval between two unix timestamps.
2432   *
2433   * E.g.
2434   * $intervalstring = get_time_interval_string(12345600, 12345660);
2435   * Will produce the string:
2436   * '0d 0h 1m'
2437   *
2438   * @param int $time1 unix timestamp
2439   * @param int $time2 unix timestamp
2440   * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2441   * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2442   */
2443  function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
2444      $dtdate = new DateTime();
2445      $dtdate->setTimeStamp($time1);
2446      $dtdate2 = new DateTime();
2447      $dtdate2->setTimeStamp($time2);
2448      $interval = $dtdate2->diff($dtdate);
2449      $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
2450      return $interval->format($format);
2451  }
2452  
2453  /**
2454   * Given a time, return the GMT timestamp of the most recent midnight
2455   * for the current user.
2456   *
2457   * @package core
2458   * @category time
2459   * @param int $date Timestamp in GMT
2460   * @param float|int|string $timezone user timezone
2461   * @return int Returns a GMT timestamp
2462   */
2463  function usergetmidnight($date, $timezone=99) {
2464  
2465      $userdate = usergetdate($date, $timezone);
2466  
2467      // Time of midnight of this user's day, in GMT.
2468      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2469  
2470  }
2471  
2472  /**
2473   * Returns a string that prints the user's timezone
2474   *
2475   * @package core
2476   * @category time
2477   * @param float|int|string $timezone user timezone
2478   * @return string
2479   */
2480  function usertimezone($timezone=99) {
2481      $tz = core_date::get_user_timezone($timezone);
2482      return core_date::get_localised_timezone($tz);
2483  }
2484  
2485  /**
2486   * Returns a float or a string which denotes the user's timezone
2487   * 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)
2488   * means that for this timezone there are also DST rules to be taken into account
2489   * Checks various settings and picks the most dominant of those which have a value
2490   *
2491   * @package core
2492   * @category time
2493   * @param float|int|string $tz timezone to calculate GMT time offset before
2494   *        calculating user timezone, 99 is default user timezone
2495   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2496   * @return float|string
2497   */
2498  function get_user_timezone($tz = 99) {
2499      global $USER, $CFG;
2500  
2501      $timezones = array(
2502          $tz,
2503          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2504          isset($USER->timezone) ? $USER->timezone : 99,
2505          isset($CFG->timezone) ? $CFG->timezone : 99,
2506          );
2507  
2508      $tz = 99;
2509  
2510      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2511      foreach ($timezones as $nextvalue) {
2512          if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2513              $tz = $nextvalue;
2514          }
2515      }
2516      return is_numeric($tz) ? (float) $tz : $tz;
2517  }
2518  
2519  /**
2520   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2521   * - Note: Daylight saving only works for string timezones and not for float.
2522   *
2523   * @package core
2524   * @category time
2525   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2526   * @param int|float|string $strtimezone user timezone
2527   * @return int
2528   */
2529  function dst_offset_on($time, $strtimezone = null) {
2530      $tz = core_date::get_user_timezone($strtimezone);
2531      $date = new DateTime('@' . $time);
2532      $date->setTimezone(new DateTimeZone($tz));
2533      if ($date->format('I') == '1') {
2534          if ($tz === 'Australia/Lord_Howe') {
2535              return 1800;
2536          }
2537          return 3600;
2538      }
2539      return 0;
2540  }
2541  
2542  /**
2543   * Calculates when the day appears in specific month
2544   *
2545   * @package core
2546   * @category time
2547   * @param int $startday starting day of the month
2548   * @param int $weekday The day when week starts (normally taken from user preferences)
2549   * @param int $month The month whose day is sought
2550   * @param int $year The year of the month whose day is sought
2551   * @return int
2552   */
2553  function find_day_in_month($startday, $weekday, $month, $year) {
2554      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2555  
2556      $daysinmonth = days_in_month($month, $year);
2557      $daysinweek = count($calendartype->get_weekdays());
2558  
2559      if ($weekday == -1) {
2560          // Don't care about weekday, so return:
2561          //    abs($startday) if $startday != -1
2562          //    $daysinmonth otherwise.
2563          return ($startday == -1) ? $daysinmonth : abs($startday);
2564      }
2565  
2566      // From now on we 're looking for a specific weekday.
2567      // Give "end of month" its actual value, since we know it.
2568      if ($startday == -1) {
2569          $startday = -1 * $daysinmonth;
2570      }
2571  
2572      // Starting from day $startday, the sign is the direction.
2573      if ($startday < 1) {
2574          $startday = abs($startday);
2575          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2576  
2577          // This is the last such weekday of the month.
2578          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2579          if ($lastinmonth > $daysinmonth) {
2580              $lastinmonth -= $daysinweek;
2581          }
2582  
2583          // Find the first such weekday <= $startday.
2584          while ($lastinmonth > $startday) {
2585              $lastinmonth -= $daysinweek;
2586          }
2587  
2588          return $lastinmonth;
2589      } else {
2590          $indexweekday = dayofweek($startday, $month, $year);
2591  
2592          $diff = $weekday - $indexweekday;
2593          if ($diff < 0) {
2594              $diff += $daysinweek;
2595          }
2596  
2597          // This is the first such weekday of the month equal to or after $startday.
2598          $firstfromindex = $startday + $diff;
2599  
2600          return $firstfromindex;
2601      }
2602  }
2603  
2604  /**
2605   * Calculate the number of days in a given month
2606   *
2607   * @package core
2608   * @category time
2609   * @param int $month The month whose day count is sought
2610   * @param int $year The year of the month whose day count is sought
2611   * @return int
2612   */
2613  function days_in_month($month, $year) {
2614      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2615      return $calendartype->get_num_days_in_month($year, $month);
2616  }
2617  
2618  /**
2619   * Calculate the position in the week of a specific calendar day
2620   *
2621   * @package core
2622   * @category time
2623   * @param int $day The day of the date whose position in the week is sought
2624   * @param int $month The month of the date whose position in the week is sought
2625   * @param int $year The year of the date whose position in the week is sought
2626   * @return int
2627   */
2628  function dayofweek($day, $month, $year) {
2629      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2630      return $calendartype->get_weekday($year, $month, $day);
2631  }
2632  
2633  // USER AUTHENTICATION AND LOGIN.
2634  
2635  /**
2636   * Returns full login url.
2637   *
2638   * Any form submissions for authentication to this URL must include username,
2639   * password as well as a logintoken generated by \core\session\manager::get_login_token().
2640   *
2641   * @return string login url
2642   */
2643  function get_login_url() {
2644      global $CFG;
2645  
2646      return "$CFG->wwwroot/login/index.php";
2647  }
2648  
2649  /**
2650   * This function checks that the current user is logged in and has the
2651   * required privileges
2652   *
2653   * This function checks that the current user is logged in, and optionally
2654   * whether they are allowed to be in a particular course and view a particular
2655   * course module.
2656   * If they are not logged in, then it redirects them to the site login unless
2657   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2658   * case they are automatically logged in as guests.
2659   * If $courseid is given and the user is not enrolled in that course then the
2660   * user is redirected to the course enrolment page.
2661   * If $cm is given and the course module is hidden and the user is not a teacher
2662   * in the course then the user is redirected to the course home page.
2663   *
2664   * When $cm parameter specified, this function sets page layout to 'module'.
2665   * You need to change it manually later if some other layout needed.
2666   *
2667   * @package    core_access
2668   * @category   access
2669   *
2670   * @param mixed $courseorid id of the course or course object
2671   * @param bool $autologinguest default true
2672   * @param object $cm course module object
2673   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2674   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2675   *             in order to keep redirects working properly. MDL-14495
2676   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2677   * @return mixed Void, exit, and die depending on path
2678   * @throws coding_exception
2679   * @throws require_login_exception
2680   * @throws moodle_exception
2681   */
2682  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2683      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2684  
2685      // Must not redirect when byteserving already started.
2686      if (!empty($_SERVER['HTTP_RANGE'])) {
2687          $preventredirect = true;
2688      }
2689  
2690      if (AJAX_SCRIPT) {
2691          // We cannot redirect for AJAX scripts either.
2692          $preventredirect = true;
2693      }
2694  
2695      // Setup global $COURSE, themes, language and locale.
2696      if (!empty($courseorid)) {
2697          if (is_object($courseorid)) {
2698              $course = $courseorid;
2699          } else if ($courseorid == SITEID) {
2700              $course = clone($SITE);
2701          } else {
2702              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2703          }
2704          if ($cm) {
2705              if ($cm->course != $course->id) {
2706                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2707              }
2708              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2709              if (!($cm instanceof cm_info)) {
2710                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2711                  // db queries so this is not really a performance concern, however it is obviously
2712                  // better if you use get_fast_modinfo to get the cm before calling this.
2713                  $modinfo = get_fast_modinfo($course);
2714                  $cm = $modinfo->get_cm($cm->id);
2715              }
2716          }
2717      } else {
2718          // Do not touch global $COURSE via $PAGE->set_course(),
2719          // the reasons is we need to be able to call require_login() at any time!!
2720          $course = $SITE;
2721          if ($cm) {
2722              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2723          }
2724      }
2725  
2726      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2727      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2728      // risk leading the user back to the AJAX request URL.
2729      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2730          $setwantsurltome = false;
2731      }
2732  
2733      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2734      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2735          if ($preventredirect) {
2736              throw new require_login_session_timeout_exception();
2737          } else {
2738              if ($setwantsurltome) {
2739                  $SESSION->wantsurl = qualified_me();
2740              }
2741              redirect(get_login_url());
2742          }
2743      }
2744  
2745      // If the user is not even logged in yet then make sure they are.
2746      if (!isloggedin()) {
2747          if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2748              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2749                  // Misconfigured site guest, just redirect to login page.
2750                  redirect(get_login_url());
2751                  exit; // Never reached.
2752              }
2753              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2754              complete_user_login($guest);
2755              $USER->autologinguest = true;
2756              $SESSION->lang = $lang;
2757          } else {
2758              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2759              if ($preventredirect) {
2760                  throw new require_login_exception('You are not logged in');
2761              }
2762  
2763              if ($setwantsurltome) {
2764                  $SESSION->wantsurl = qualified_me();
2765              }
2766  
2767              // Give auth plugins an opportunity to authenticate or redirect to an external login page
2768              $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2769              foreach($authsequence as $authname) {
2770                  $authplugin = get_auth_plugin($authname);
2771                  $authplugin->pre_loginpage_hook();
2772                  if (isloggedin()) {
2773                      if ($cm) {
2774                          $modinfo = get_fast_modinfo($course);
2775                          $cm = $modinfo->get_cm($cm->id);
2776                      }
2777                      set_access_log_user();
2778                      break;
2779                  }
2780              }
2781  
2782              // If we're still not logged in then go to the login page
2783              if (!isloggedin()) {
2784                  redirect(get_login_url());
2785                  exit; // Never reached.
2786              }
2787          }
2788      }
2789  
2790      // Loginas as redirection if needed.
2791      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2792          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2793              if ($USER->loginascontext->instanceid != $course->id) {
2794                  print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2795              }
2796          }
2797      }
2798  
2799      // Check whether the user should be changing password (but only if it is REALLY them).
2800      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2801          $userauth = get_auth_plugin($USER->auth);
2802          if ($userauth->can_change_password() and !$preventredirect) {
2803              if ($setwantsurltome) {
2804                  $SESSION->wantsurl = qualified_me();
2805              }
2806              if ($changeurl = $userauth->change_password_url()) {
2807                  // Use plugin custom url.
2808                  redirect($changeurl);
2809              } else {
2810                  // Use moodle internal method.
2811                  redirect($CFG->wwwroot .'/login/change_password.php');
2812              }
2813          } else if ($userauth->can_change_password()) {
2814              throw new moodle_exception('forcepasswordchangenotice');
2815          } else {
2816              throw new moodle_exception('nopasswordchangeforced', 'auth');
2817          }
2818      }
2819  
2820      // Check that the user account is properly set up. If we can't redirect to
2821      // edit their profile and this is not a WS request, perform just the lax check.
2822      // It will allow them to use filepicker on the profile edit page.
2823  
2824      if ($preventredirect && !WS_SERVER) {
2825          $usernotfullysetup = user_not_fully_set_up($USER, false);
2826      } else {
2827          $usernotfullysetup = user_not_fully_set_up($USER, true);
2828      }
2829  
2830      if ($usernotfullysetup) {
2831          if ($preventredirect) {
2832              throw new moodle_exception('usernotfullysetup');
2833          }
2834          if ($setwantsurltome) {
2835              $SESSION->wantsurl = qualified_me();
2836          }
2837          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2838      }
2839  
2840      // Make sure the USER has a sesskey set up. Used for CSRF protection.
2841      sesskey();
2842  
2843      if (\core\session\manager::is_loggedinas()) {
2844          // During a "logged in as" session we should force all content to be cleaned because the
2845          // logged in user will be viewing potentially malicious user generated content.
2846          // See MDL-63786 for more details.
2847          $CFG->forceclean = true;
2848      }
2849  
2850      $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2851  
2852      // Do not bother admins with any formalities, except for activities pending deletion.
2853      if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2854          // Set the global $COURSE.
2855          if ($cm) {
2856              $PAGE->set_cm($cm, $course);
2857              $PAGE->set_pagelayout('incourse');
2858          } else if (!empty($courseorid)) {
2859              $PAGE->set_course($course);
2860          }
2861          // Set accesstime or the user will appear offline which messes up messaging.
2862          // Do not update access time for webservice or ajax requests.
2863          if (!WS_SERVER && !AJAX_SCRIPT) {
2864              user_accesstime_log($course->id);
2865          }
2866  
2867          foreach ($afterlogins as $plugintype => $plugins) {
2868              foreach ($plugins as $pluginfunction) {
2869                  $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2870              }
2871          }
2872          return;
2873      }
2874  
2875      // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2876      // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2877      if (!defined('NO_SITEPOLICY_CHECK')) {
2878          define('NO_SITEPOLICY_CHECK', false);
2879      }
2880  
2881      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2882      // Do not test if the script explicitly asked for skipping the site policies check.
2883      // Or if the user auth type is webservice.
2884      if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2885          $manager = new \core_privacy\local\sitepolicy\manager();
2886          if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2887              if ($preventredirect) {
2888                  throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2889              }
2890              if ($setwantsurltome) {
2891                  $SESSION->wantsurl = qualified_me();
2892              }
2893              redirect($policyurl);
2894          }
2895      }
2896  
2897      // Fetch the system context, the course context, and prefetch its child contexts.
2898      $sysctx = context_system::instance();
2899      $coursecontext = context_course::instance($course->id, MUST_EXIST);
2900      if ($cm) {
2901          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2902      } else {
2903          $cmcontext = null;
2904      }
2905  
2906      // If the site is currently under maintenance, then print a message.
2907      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2908          if ($preventredirect) {
2909              throw new require_login_exception('Maintenance in progress');
2910          }
2911          $PAGE->set_context(null);
2912          print_maintenance_message();
2913      }
2914  
2915      // Make sure the course itself is not hidden.
2916      if ($course->id == SITEID) {
2917          // Frontpage can not be hidden.
2918      } else {
2919          if (is_role_switched($course->id)) {
2920              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2921          } else {
2922              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2923                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2924                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2925                  if ($preventredirect) {
2926                      throw new require_login_exception('Course is hidden');
2927                  }
2928                  $PAGE->set_context(null);
2929                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
2930                  // the navigation will mess up when trying to find it.
2931                  navigation_node::override_active_url(new moodle_url('/'));
2932                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2933              }
2934          }
2935      }
2936  
2937      // Is the user enrolled?
2938      if ($course->id == SITEID) {
2939          // Everybody is enrolled on the frontpage.
2940      } else {
2941          if (\core\session\manager::is_loggedinas()) {
2942              // Make sure the REAL person can access this course first.
2943              $realuser = \core\session\manager::get_realuser();
2944              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2945                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2946                  if ($preventredirect) {
2947                      throw new require_login_exception('Invalid course login-as access');
2948                  }
2949                  $PAGE->set_context(null);
2950                  echo $OUTPUT->header();
2951                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2952              }
2953          }
2954  
2955          $access = false;
2956  
2957          if (is_role_switched($course->id)) {
2958              // Ok, user had to be inside this course before the switch.
2959              $access = true;
2960  
2961          } else if (is_viewing($coursecontext, $USER)) {
2962              // Ok, no need to mess with enrol.
2963              $access = true;
2964  
2965          } else {
2966              if (isset($USER->enrol['enrolled'][$course->id])) {
2967                  if ($USER->enrol['enrolled'][$course->id] > time()) {
2968                      $access = true;
2969                      if (isset($USER->enrol['tempguest'][$course->id])) {
2970                          unset($USER->enrol['tempguest'][$course->id]);
2971                          remove_temp_course_roles($coursecontext);
2972                      }
2973                  } else {
2974                      // Expired.
2975                      unset($USER->enrol['enrolled'][$course->id]);
2976                  }
2977              }
2978              if (isset($USER->enrol['tempguest'][$course->id])) {
2979                  if ($USER->enrol['tempguest'][$course->id] == 0) {
2980                      $access = true;
2981                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2982                      $access = true;
2983                  } else {
2984                      // Expired.
2985                      unset($USER->enrol['tempguest'][$course->id]);
2986                      remove_temp_course_roles($coursecontext);
2987                  }
2988              }
2989  
2990              if (!$access) {
2991                  // Cache not ok.
2992                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2993                  if ($until !== false) {
2994                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
2995                      if ($until == 0) {
2996                          $until = ENROL_MAX_TIMESTAMP;
2997                      }
2998                      $USER->enrol['enrolled'][$course->id] = $until;
2999                      $access = true;
3000  
3001                  } else if (core_course_category::can_view_course_info($course)) {
3002                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
3003                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3004                      $enrols = enrol_get_plugins(true);
3005                      // First ask all enabled enrol instances in course if they want to auto enrol user.
3006                      foreach ($instances as $instance) {
3007                          if (!isset($enrols[$instance->enrol])) {
3008                              continue;
3009                          }
3010                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3011                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3012                          if ($until !== false) {
3013                              if ($until == 0) {
3014                                  $until = ENROL_MAX_TIMESTAMP;
3015                              }
3016                              $USER->enrol['enrolled'][$course->id] = $until;
3017                              $access = true;
3018                              break;
3019                          }
3020                      }
3021                      // If not enrolled yet try to gain temporary guest access.
3022                      if (!$access) {
3023                          foreach ($instances as $instance) {
3024                              if (!isset($enrols[$instance->enrol])) {
3025                                  continue;
3026                              }
3027                              // Get a duration for the guest access, a timestamp in the future or false.
3028                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3029                              if ($until !== false and $until > time()) {
3030                                  $USER->enrol['tempguest'][$course->id] = $until;
3031                                  $access = true;
3032                                  break;
3033                              }
3034                          }
3035                      }
3036                  } else {
3037                      // User is not enrolled and is not allowed to browse courses here.
3038                      if ($preventredirect) {
3039                          throw new require_login_exception('Course is not available');
3040                      }
3041                      $PAGE->set_context(null);
3042                      // We need to override the navigation URL as the course won't have been added to the navigation and thus
3043                      // the navigation will mess up when trying to find it.
3044                      navigation_node::override_active_url(new moodle_url('/'));
3045                      notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3046                  }
3047              }
3048          }
3049  
3050          if (!$access) {
3051              if ($preventredirect) {
3052                  throw new require_login_exception('Not enrolled');
3053              }
3054              if ($setwantsurltome) {
3055                  $SESSION->wantsurl = qualified_me();
3056              }
3057              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3058          }
3059      }
3060  
3061      // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3062      if ($cm && $cm->deletioninprogress) {
3063          if ($preventredirect) {
3064              throw new moodle_exception('activityisscheduledfordeletion');
3065          }
3066          require_once($CFG->dirroot . '/course/lib.php');
3067          redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3068      }
3069  
3070      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3071      if ($cm && !$cm->uservisible) {
3072          if ($preventredirect) {
3073              throw new require_login_exception('Activity is hidden');
3074          }
3075          // Get the error message that activity is not available and why (if explanation can be shown to the user).
3076          $PAGE->set_course($course);
3077          $renderer = $PAGE->get_renderer('course');
3078          $message = $renderer->course_section_cm_unavailable_error_message($cm);
3079          redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3080      }
3081  
3082      // Set the global $COURSE.
3083      if ($cm) {
3084          $PAGE->set_cm($cm, $course);
3085          $PAGE->set_pagelayout('incourse');
3086      } else if (!empty($courseorid)) {
3087          $PAGE->set_course($course);
3088      }
3089  
3090      foreach ($afterlogins as $plugintype => $plugins) {
3091          foreach ($plugins as $pluginfunction) {
3092              $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3093          }
3094      }
3095  
3096      // Finally access granted, update lastaccess times.
3097      // Do not update access time for webservice or ajax requests.
3098      if (!WS_SERVER && !AJAX_SCRIPT) {
3099          user_accesstime_log($course->id);
3100      }
3101  }
3102  
3103  /**
3104   * A convenience function for where we must be logged in as admin
3105   * @return void
3106   */
3107  function require_admin() {
3108      require_login(null, false);
3109      require_capability('moodle/site:config', context_system::instance());
3110  }
3111  
3112  /**
3113   * This function just makes sure a user is logged out.
3114   *
3115   * @package    core_access
3116   * @category   access
3117   */
3118  function require_logout() {
3119      global $USER, $DB;
3120  
3121      if (!isloggedin()) {
3122          // This should not happen often, no need for hooks or events here.
3123          \core\session\manager::terminate_current();
3124          return;
3125      }
3126  
3127      // Execute hooks before action.
3128      $authplugins = array();
3129      $authsequence = get_enabled_auth_plugins();
3130      foreach ($authsequence as $authname) {
3131          $authplugins[$authname] = get_auth_plugin($authname);
3132          $authplugins[$authname]->prelogout_hook();
3133      }
3134  
3135      // Store info that gets removed during logout.
3136      $sid = session_id();
3137      $event = \core\event\user_loggedout::create(
3138          array(
3139              'userid' => $USER->id,
3140              'objectid' => $USER->id,
3141              'other' => array('sessionid' => $sid),
3142          )
3143      );
3144      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3145          $event->add_record_snapshot('sessions', $session);
3146      }
3147  
3148      // Clone of $USER object to be used by auth plugins.
3149      $user = fullclone($USER);
3150  
3151      // Delete session record and drop $_SESSION content.
3152      \core\session\manager::terminate_current();
3153  
3154      // Trigger event AFTER action.
3155      $event->trigger();
3156  
3157      // Hook to execute auth plugins redirection after event trigger.
3158      foreach ($authplugins as $authplugin) {
3159          $authplugin->postlogout_hook($user);
3160      }
3161  }
3162  
3163  /**
3164   * Weaker version of require_login()
3165   *
3166   * This is a weaker version of {@link require_login()} which only requires login
3167   * when called from within a course rather than the site page, unless
3168   * the forcelogin option is turned on.
3169   * @see require_login()
3170   *
3171   * @package    core_access
3172   * @category   access
3173   *
3174   * @param mixed $courseorid The course object or id in question
3175   * @param bool $autologinguest Allow autologin guests if that is wanted
3176   * @param object $cm Course activity module if known
3177   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3178   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3179   *             in order to keep redirects working properly. MDL-14495
3180   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3181   * @return void
3182   * @throws coding_exception
3183   */
3184  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3185      global $CFG, $PAGE, $SITE;
3186      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3187            or (!is_object($courseorid) and $courseorid == SITEID));
3188      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3189          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3190          // db queries so this is not really a performance concern, however it is obviously
3191          // better if you use get_fast_modinfo to get the cm before calling this.
3192          if (is_object($courseorid)) {
3193              $course = $courseorid;
3194          } else {
3195              $course = clone($SITE);
3196          }
3197          $modinfo = get_fast_modinfo($course);
3198          $cm = $modinfo->get_cm($cm->id);
3199      }
3200      if (!empty($CFG->forcelogin)) {
3201          // Login required for both SITE and courses.
3202          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3203  
3204      } else if ($issite && !empty($cm) and !$cm->uservisible) {
3205          // Always login for hidden activities.
3206          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3207  
3208      } else if (isloggedin() && !isguestuser()) {
3209          // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3210          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3211  
3212      } else if ($issite) {
3213          // Login for SITE not required.
3214          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3215          if (!empty($courseorid)) {
3216              if (is_object($courseorid)) {
3217                  $course = $courseorid;
3218              } else {
3219                  $course = clone $SITE;
3220              }
3221              if ($cm) {
3222                  if ($cm->course != $course->id) {
3223                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3224                  }
3225                  $PAGE->set_cm($cm, $course);
3226                  $PAGE->set_pagelayout('incourse');
3227              } else {
3228                  $PAGE->set_course($course);
3229              }
3230          } else {
3231              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3232              $PAGE->set_course($PAGE->course);
3233          }
3234          // Do not update access time for webservice or ajax requests.
3235          if (!WS_SERVER && !AJAX_SCRIPT) {
3236              user_accesstime_log(SITEID);
3237          }
3238          return;
3239  
3240      } else {
3241          // Course login always required.
3242          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3243      }
3244  }
3245  
3246  /**
3247   * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3248   *
3249   * @param  string $keyvalue the key value
3250   * @param  string $script   unique script identifier
3251   * @param  int $instance    instance id
3252   * @return stdClass the key entry in the user_private_key table
3253   * @since Moodle 3.2
3254   * @throws moodle_exception
3255   */
3256  function validate_user_key($keyvalue, $script, $instance) {
3257      global $DB;
3258  
3259      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3260          print_error('invalidkey');
3261      }
3262  
3263      if (!empty($key->validuntil) and $key->validuntil < time()) {
3264          print_error('expiredkey');
3265      }
3266  
3267      if ($key->iprestriction) {
3268          $remoteaddr = getremoteaddr(null);
3269          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3270              print_error('ipmismatch');
3271          }
3272      }
3273      return $key;
3274  }
3275  
3276  /**
3277   * Require key login. Function terminates with error if key not found or incorrect.
3278   *
3279   * @uses NO_MOODLE_COOKIES
3280   * @uses PARAM_ALPHANUM
3281   * @param string $script unique script identifier
3282   * @param int $instance optional instance id
3283   * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3284   * @return int Instance ID
3285   */
3286  function require_user_key_login($script, $instance = null, $keyvalue = null) {
3287      global $DB;
3288  
3289      if (!NO_MOODLE_COOKIES) {
3290          print_error('sessioncookiesdisable');
3291      }
3292  
3293      // Extra safety.
3294      \core\session\manager::write_close();
3295  
3296      if (null === $keyvalue) {
3297          $keyvalue = required_param('key', PARAM_ALPHANUM);
3298      }
3299  
3300      $key = validate_user_key($keyvalue, $script, $instance);
3301  
3302      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3303          print_error('invaliduserid');
3304      }
3305  
3306      core_user::require_active_user($user, true, true);
3307  
3308      // Emulate normal session.
3309      enrol_check_plugins($user);
3310      \core\session\manager::set_user($user);
3311  
3312      // Note we are not using normal login.
3313      if (!defined('USER_KEY_LOGIN')) {
3314          define('USER_KEY_LOGIN', true);
3315      }
3316  
3317      // Return instance id - it might be empty.
3318      return $key->instance;
3319  }
3320  
3321  /**
3322   * Creates a new private user access key.
3323   *
3324   * @param string $script unique target identifier
3325   * @param int $userid
3326   * @param int $instance optional instance id
3327   * @param string $iprestriction optional ip restricted access
3328   * @param int $validuntil key valid only until given data
3329   * @return string access key value
3330   */
3331  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3332      global $DB;
3333  
3334      $key = new stdClass();
3335      $key->script        = $script;
3336      $key->userid        = $userid;
3337      $key->instance      = $instance;
3338      $key->iprestriction = $iprestriction;
3339      $key->validuntil    = $validuntil;
3340      $key->timecreated   = time();
3341  
3342      // Something long and unique.
3343      $key->value         = md5($userid.'_'.time().random_string(40));
3344      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3345          // Must be unique.
3346          $key->value     = md5($userid.'_'.time().random_string(40));
3347      }
3348      $DB->insert_record('user_private_key', $key);
3349      return $key->value;
3350  }
3351  
3352  /**
3353   * Delete the user's new private user access keys for a particular script.
3354   *
3355   * @param string $script unique target identifier
3356   * @param int $userid
3357   * @return void
3358   */
3359  function delete_user_key($script, $userid) {
3360      global $DB;
3361      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3362  }
3363  
3364  /**
3365   * Gets a private user access key (and creates one if one doesn't exist).
3366   *
3367   * @param string $script unique target identifier
3368   * @param int $userid
3369   * @param int $instance optional instance id
3370   * @param string $iprestriction optional ip restricted access
3371   * @param int $validuntil key valid only until given date
3372   * @return string access key value
3373   */
3374  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3375      global $DB;
3376  
3377      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3378                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
3379                                                           'validuntil' => $validuntil))) {
3380          return $key->value;
3381      } else {
3382          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3383      }
3384  }
3385  
3386  
3387  /**
3388   * Modify the user table by setting the currently logged in user's last login to now.
3389   *
3390   * @return bool Always returns true
3391   */
3392  function update_user_login_times() {
3393      global $USER, $DB, $SESSION;
3394  
3395      if (isguestuser()) {
3396          // Do not update guest access times/ips for performance.
3397          return true;
3398      }
3399  
3400      if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
3401          // Do not update user login time when using user key login.
3402          return true;
3403      }
3404  
3405      $now = time();
3406  
3407      $user = new stdClass();
3408      $user->id = $USER->id;
3409  
3410      // Make sure all users that logged in have some firstaccess.
3411      if ($USER->firstaccess == 0) {
3412          $USER->firstaccess = $user->firstaccess = $now;
3413      }
3414  
3415      // Store the previous current as lastlogin.
3416      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3417  
3418      $USER->currentlogin = $user->currentlogin = $now;
3419  
3420      // Function user_accesstime_log() may not update immediately, better do it here.
3421      $USER->lastaccess = $user->lastaccess = $now;
3422      $SESSION->userpreviousip = $USER->lastip;
3423      $USER->lastip = $user->lastip = getremoteaddr();
3424  
3425      // Note: do not call user_update_user() here because this is part of the login process,
3426      //       the login event means that these fields were updated.
3427      $DB->update_record('user', $user);
3428      return true;
3429  }
3430  
3431  /**
3432   * Determines if a user has completed setting up their account.
3433   *
3434   * The lax mode (with $strict = false) has been introduced for special cases
3435   * only where we want to skip certain checks intentionally. This is valid in
3436   * certain mnet or ajax scenarios when the user cannot / should not be
3437   * redirected to edit their profile. In most cases, you should perform the
3438   * strict check.
3439   *
3440   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3441   * @param bool $strict Be more strict and assert id and custom profile fields set, too
3442   * @return bool
3443   */
3444  function user_not_fully_set_up($user, $strict = true) {
3445      global $CFG;
3446      require_once($CFG->dirroot.'/user/profile/lib.php');
3447  
3448      if (isguestuser($user)) {
3449          return false;
3450      }
3451  
3452      if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3453          return true;
3454      }
3455  
3456      if ($strict) {
3457          if (empty($user->id)) {
3458              // Strict mode can be used with existing accounts only.
3459              return true;
3460          }
3461          if (!profile_has_required_custom_fields_set($user->id)) {
3462              return true;
3463          }
3464      }
3465  
3466      return false;
3467  }
3468  
3469  /**
3470   * Check whether the user has exceeded the bounce threshold
3471   *
3472   * @param stdClass $user A {@link $USER} object
3473   * @return bool true => User has exceeded bounce threshold
3474   */
3475  function over_bounce_threshold($user) {
3476      global $CFG, $DB;
3477  
3478      if (empty($CFG->handlebounces)) {
3479          return false;
3480      }
3481  
3482      if (empty($user->id)) {
3483          // No real (DB) user, nothing to do here.
3484          return false;
3485      }
3486  
3487      // Set sensible defaults.
3488      if (empty($CFG->minbounces)) {
3489          $CFG->minbounces = 10;
3490      }
3491      if (empty($CFG->bounceratio)) {
3492          $CFG->bounceratio = .20;
3493      }
3494      $bouncecount = 0;
3495      $sendcount = 0;
3496      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3497          $bouncecount = $bounce->value;
3498      }
3499      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3500          $sendcount = $send->value;
3501      }
3502      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3503  }
3504  
3505  /**
3506   * Used to increment or reset email sent count
3507   *
3508   * @param stdClass $user object containing an id
3509   * @param bool $reset will reset the count to 0
3510   * @return void
3511   */
3512  function set_send_count($user, $reset=false) {
3513      global $DB;
3514  
3515      if (empty($user->id)) {
3516          // No real (DB) user, nothing to do here.
3517          return;
3518      }
3519  
3520      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3521          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3522          $DB->update_record('user_preferences', $pref);
3523      } else if (!empty($reset)) {
3524          // If it's not there and we're resetting, don't bother. Make a new one.
3525          $pref = new stdClass();
3526          $pref->name   = 'email_send_count';
3527          $pref->value  = 1;
3528          $pref->userid = $user->id;
3529          $DB->insert_record('user_preferences', $pref, false);
3530      }
3531  }
3532  
3533  /**
3534   * Increment or reset user's email bounce count
3535   *
3536   * @param stdClass $user object containing an id
3537   * @param bool $reset will reset the count to 0
3538   */
3539  function set_bounce_count($user, $reset=false) {
3540      global $DB;
3541  
3542      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_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_bounce_count';
3549          $pref->value  = 1;
3550          $pref->userid = $user->id;
3551          $DB->insert_record('user_preferences', $pref, false);
3552      }
3553  }
3554  
3555  /**
3556   * Determines if the logged in user is currently moving an activity
3557   *
3558   * @param int $courseid The id of the course being tested
3559   * @return bool
3560   */
3561  function ismoving($courseid) {
3562      global $USER;
3563  
3564      if (!empty($USER->activitycopy)) {
3565          return ($USER->activitycopycourse == $courseid);
3566      }
3567      return false;
3568  }
3569  
3570  /**
3571   * Returns a persons full name
3572   *
3573   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3574   * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3575   * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3576   * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3577   *
3578   * @param stdClass $user A {@link $USER} object to get full name of.
3579   * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3580   * @return string
3581   */
3582  function fullname($user, $override=false) {
3583      global $CFG, $SESSION;
3584  
3585      if (!isset($user->firstname) and !isset($user->lastname)) {
3586          return '';
3587      }
3588  
3589      // Get all of the name fields.
3590      $allnames = \core_user\fields::get_name_fields();
3591      if ($CFG->debugdeveloper) {
3592          foreach ($allnames as $allname) {
3593              if (!property_exists($user, $allname)) {
3594                  // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
3595                  debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3596                  // Message has been sent, no point in sending the message multiple times.
3597                  break;
3598              }
3599          }
3600      }
3601  
3602      if (!$override) {
3603          if (!empty($CFG->forcefirstname)) {
3604              $user->firstname = $CFG->forcefirstname;
3605          }
3606          if (!empty($CFG->forcelastname)) {
3607              $user->lastname = $CFG->forcelastname;
3608          }
3609      }
3610  
3611      if (!empty($SESSION->fullnamedisplay)) {
3612          $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3613      }
3614  
3615      $template = null;
3616      // If the fullnamedisplay setting is available, set the template to that.
3617      if (isset($CFG->fullnamedisplay)) {
3618          $template = $CFG->fullnamedisplay;
3619      }
3620      // If the template is empty, or set to language, return the language string.
3621      if ((empty($template) || $template == 'language') && !$override) {
3622          return get_string('fullnamedisplay', null, $user);
3623      }
3624  
3625      // Check to see if we are displaying according to the alternative full name format.
3626      if ($override) {
3627          if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
3628              // Default to show just the user names according to the fullnamedisplay string.
3629              return get_string('fullnamedisplay', null, $user);
3630          } else {
3631              // If the override is true, then change the template to use the complete name.
3632              $template = $CFG->alternativefullnameformat;
3633          }
3634      }
3635  
3636      $requirednames = array();
3637      // With each name, see if it is in the display name template, and add it to the required names array if it is.
3638      foreach ($allnames as $allname) {
3639          if (strpos($template, $allname) !== false) {
3640              $requirednames[] = $allname;
3641          }
3642      }
3643  
3644      $displayname = $template;
3645      // Switch in the actual data into the template.
3646      foreach ($requirednames as $altname) {
3647          if (isset($user->$altname)) {
3648              // Using empty() on the below if statement causes breakages.
3649              if ((string)$user->$altname == '') {
3650                  $displayname = str_replace($altname, 'EMPTY', $displayname);
3651              } else {
3652                  $displayname = str_replace($altname, $user->$altname, $displayname);
3653              }
3654          } else {
3655              $displayname = str_replace($altname, 'EMPTY', $displayname);
3656          }
3657      }
3658      // Tidy up any misc. characters (Not perfect, but gets most characters).
3659      // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3660      // katakana and parenthesis.
3661      $patterns = array();
3662      // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3663      // filled in by a user.
3664      // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3665      $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3666      // This regular expression is to remove any double spaces in the display name.
3667      $patterns[] = '/\s{2,}/u';
3668      foreach ($patterns as $pattern) {
3669          $displayname = preg_replace($pattern, ' ', $displayname);
3670      }
3671  
3672      // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3673      $displayname = trim($displayname);
3674      if (empty($displayname)) {
3675          // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3676          // people in general feel is a good setting to fall back on.
3677          $displayname = $user->firstname;
3678      }
3679      return $displayname;
3680  }
3681  
3682  /**
3683   * Reduces lines of duplicated code for getting user name fields.
3684   *
3685   * See also {@link user_picture::unalias()}
3686   *
3687   * @param object $addtoobject Object to add user name fields to.
3688   * @param object $secondobject Object that contains user name field information.
3689   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3690   * @param array $additionalfields Additional fields to be matched with data in the second object.
3691   * The key can be set to the user table field name.
3692   * @return object User name fields.
3693   */
3694  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3695      $fields = [];
3696      foreach (\core_user\fields::get_name_fields() as $field) {
3697          $fields[$field] = $prefix . $field;
3698      }
3699      if ($additionalfields) {
3700          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3701          // the key is a number and then sets the key to the array value.
3702          foreach ($additionalfields as $key => $value) {
3703              if (is_numeric($key)) {
3704                  $additionalfields[$value] = $prefix . $value;
3705                  unset($additionalfields[$key]);
3706              } else {
3707                  $additionalfields[$key] = $prefix . $value;
3708              }
3709          }
3710          $fields = array_merge($fields, $additionalfields);
3711      }
3712      foreach ($fields as $key => $field) {
3713          // Important that we have all of the user name fields present in the object that we are sending back.
3714          $addtoobject->$key = '';
3715          if (isset($secondobject->$field)) {
3716              $addtoobject->$key = $secondobject->$field;
3717          }
3718      }
3719      return $addtoobject;
3720  }
3721  
3722  /**
3723   * Returns an array of values in order of occurance in a provided string.
3724   * The key in the result is the character postion in the string.
3725   *
3726   * @param array $values Values to be found in the string format
3727   * @param string $stringformat The string which may contain values being searched for.
3728   * @return array An array of values in order according to placement in the string format.
3729   */
3730  function order_in_string($values, $stringformat) {
3731      $valuearray = array();
3732      foreach ($values as $value) {
3733          $pattern = "/$value\b/";
3734          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3735          if (preg_match($pattern, $stringformat)) {
3736              $replacement = "thing";
3737              // Replace the value with something more unique to ensure we get the right position when using strpos().
3738              $newformat = preg_replace($pattern, $replacement, $stringformat);
3739              $position = strpos($newformat, $replacement);
3740              $valuearray[$position] = $value;
3741          }
3742      }
3743      ksort($valuearray);
3744      return $valuearray;
3745  }
3746  
3747  /**
3748   * Returns whether a given authentication plugin exists.
3749   *
3750   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3751   * @return boolean Whether the plugin is available.
3752   */
3753  function exists_auth_plugin($auth) {
3754      global $CFG;
3755  
3756      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3757          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3758      }
3759      return false;
3760  }
3761  
3762  /**
3763   * Checks if a given plugin is in the list of enabled authentication plugins.
3764   *
3765   * @param string $auth Authentication plugin.
3766   * @return boolean Whether the plugin is enabled.
3767   */
3768  function is_enabled_auth($auth) {
3769      if (empty($auth)) {
3770          return false;
3771      }
3772  
3773      $enabled = get_enabled_auth_plugins();
3774  
3775      return in_array($auth, $enabled);
3776  }
3777  
3778  /**
3779   * Returns an authentication plugin instance.
3780   *
3781   * @param string $auth name of authentication plugin
3782   * @return auth_plugin_base An instance of the required authentication plugin.
3783   */
3784  function get_auth_plugin($auth) {
3785      global $CFG;
3786  
3787      // Check the plugin exists first.
3788      if (! exists_auth_plugin($auth)) {
3789          print_error('authpluginnotfound', 'debug', '', $auth);
3790      }
3791  
3792      // Return auth plugin instance.
3793      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3794      $class = "auth_plugin_$auth";
3795      return new $class;
3796  }
3797  
3798  /**
3799   * Returns array of active auth plugins.
3800   *
3801   * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3802   * @return array
3803   */
3804  function get_enabled_auth_plugins($fix=false) {
3805      global $CFG;
3806  
3807      $default = array('manual', 'nologin');
3808  
3809      if (empty($CFG->auth)) {
3810          $auths = array();
3811      } else {
3812          $auths = explode(',', $CFG->auth);
3813      }
3814  
3815      $auths = array_unique($auths);
3816      $oldauthconfig = implode(',', $auths);
3817      foreach ($auths as $k => $authname) {
3818          if (in_array($authname, $default)) {
3819              // The manual and nologin plugin never need to be stored.
3820              unset($auths[$k]);
3821          } else if (!exists_auth_plugin($authname)) {
3822              debugging(get_string('authpluginnotfound', 'debug', $authname));
3823              unset($auths[$k]);
3824          }
3825      }
3826  
3827      // Ideally only explicit interaction from a human admin should trigger a
3828      // change in auth config, see MDL-70424 for details.
3829      if ($fix) {
3830          $newconfig = implode(',', $auths);
3831          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3832              add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3833              set_config('auth', $newconfig);
3834          }
3835      }
3836  
3837      return (array_merge($default, $auths));
3838  }
3839  
3840  /**
3841   * Returns true if an internal authentication method is being used.
3842   * if method not specified then, global default is assumed
3843   *
3844   * @param string $auth Form of authentication required
3845   * @return bool
3846   */
3847  function is_internal_auth($auth) {
3848      // Throws error if bad $auth.
3849      $authplugin = get_auth_plugin($auth);
3850      return $authplugin->is_internal();
3851  }
3852  
3853  /**
3854   * Returns true if the user is a 'restored' one.
3855   *
3856   * Used in the login process to inform the user and allow him/her to reset the password
3857   *
3858   * @param string $username username to be checked
3859   * @return bool
3860   */
3861  function is_restored_user($username) {
3862      global $CFG, $DB;
3863  
3864      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3865  }
3866  
3867  /**
3868   * Returns an array of user fields
3869   *
3870   * @return array User field/column names
3871   */
3872  function get_user_fieldnames() {
3873      global $DB;
3874  
3875      $fieldarray = $DB->get_columns('user');
3876      unset($fieldarray['id']);
3877      $fieldarray = array_keys($fieldarray);
3878  
3879      return $fieldarray;
3880  }
3881  
3882  /**
3883   * Returns the string of the language for the new user.
3884   *
3885   * @return string language for the new user
3886   */
3887  function get_newuser_language() {
3888      global $CFG, $SESSION;
3889      return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3890  }
3891  
3892  /**
3893   * Creates a bare-bones user record
3894   *
3895   * @todo Outline auth types and provide code example
3896   *
3897   * @param string $username New user's username to add to record
3898   * @param string $password New user's password to add to record
3899   * @param string $auth Form of authentication required
3900   * @return stdClass A complete user object
3901   */
3902  function create_user_record($username, $password, $auth = 'manual') {
3903      global $CFG, $DB, $SESSION;
3904      require_once($CFG->dirroot.'/user/profile/lib.php');
3905      require_once($CFG->dirroot.'/user/lib.php');
3906  
3907      // Just in case check text case.
3908      $username = trim(core_text::strtolower($username));
3909  
3910      $authplugin = get_auth_plugin($auth);
3911      $customfields = $authplugin->get_custom_user_profile_fields();
3912      $newuser = new stdClass();
3913      if ($newinfo = $authplugin->get_userinfo($username)) {
3914          $newinfo = truncate_userinfo($newinfo);
3915          foreach ($newinfo as $key => $value) {
3916              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3917                  $newuser->$key = $value;
3918              }
3919          }
3920      }
3921  
3922      if (!empty($newuser->email)) {
3923          if (email_is_not_allowed($newuser->email)) {
3924              unset($newuser->email);
3925          }
3926      }
3927  
3928      $newuser->auth = $auth;
3929      $newuser->username = $username;
3930  
3931      // Fix for MDL-8480
3932      // user CFG lang for user if $newuser->lang is empty
3933      // or $user->lang is not an installed language.
3934      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3935          $newuser->lang = get_newuser_language();
3936      }
3937      $newuser->confirmed = 1;
3938      $newuser->lastip = getremoteaddr();
3939      $newuser->timecreated = time();
3940      $newuser->timemodified = $newuser->timecreated;
3941      $newuser->mnethostid = $CFG->mnet_localhost_id;
3942  
3943      $newuser->id = user_create_user($newuser, false, false);
3944  
3945      // Save user profile data.
3946      profile_save_data($newuser);
3947  
3948      $user = get_complete_user_data('id', $newuser->id);
3949      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3950          set_user_preference('auth_forcepasswordchange', 1, $user);
3951      }
3952      // Set the password.
3953      update_internal_user_password($user, $password);
3954  
3955      // Trigger event.
3956      \core\event\user_created::create_from_userid($newuser->id)->trigger();
3957  
3958      return $user;
3959  }
3960  
3961  /**
3962   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3963   *
3964   * @param string $username user's username to update the record
3965   * @return stdClass A complete user object
3966   */
3967  function update_user_record($username) {
3968      global $DB, $CFG;
3969      // Just in case check text case.
3970      $username = trim(core_text::strtolower($username));
3971  
3972      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3973      return update_user_record_by_id($oldinfo->id);
3974  }
3975  
3976  /**
3977   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3978   *
3979   * @param int $id user id
3980   * @return stdClass A complete user object
3981   */
3982  function update_user_record_by_id($id) {
3983      global $DB, $CFG;
3984      require_once($CFG->dirroot."/user/profile/lib.php");
3985      require_once($CFG->dirroot.'/user/lib.php');
3986  
3987      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3988      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3989  
3990      $newuser = array();
3991      $userauth = get_auth_plugin($oldinfo->auth);
3992  
3993      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3994          $newinfo = truncate_userinfo($newinfo);
3995          $customfields = $userauth->get_custom_user_profile_fields();
3996  
3997          foreach ($newinfo as $key => $value) {
3998              $iscustom = in_array($key, $customfields);
3999              if (!$iscustom) {
4000                  $key = strtolower($key);
4001              }
4002              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4003                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4004                  // Unknown or must not be changed.
4005                  continue;
4006              }
4007              if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
4008                  continue;
4009              }
4010              $confval = $userauth->config->{'field_updatelocal_' . $key};
4011              $lockval = $userauth->config->{'field_lock_' . $key};
4012              if ($confval === 'onlogin') {
4013                  // MDL-4207 Don't overwrite modified user profile values with
4014                  // empty LDAP values when 'unlocked if empty' is set. The purpose
4015                  // of the setting 'unlocked if empty' is to allow the user to fill
4016                  // in a value for the selected field _if LDAP is giving
4017                  // nothing_ for this field. Thus it makes sense to let this value
4018                  // stand in until LDAP is giving a value for this field.
4019                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
4020                      if ($iscustom || (in_array($key, $userauth->userfields) &&
4021                              ((string)$oldinfo->$key !== (string)$value))) {
4022                          $newuser[$key] = (string)$value;
4023                      }
4024                  }
4025              }
4026          }
4027          if ($newuser) {
4028              $newuser['id'] = $oldinfo->id;
4029              $newuser['timemodified'] = time();
4030              user_update_user((object) $newuser, false, false);
4031  
4032              // Save user profile data.
4033              profile_save_data((object) $newuser);
4034  
4035              // Trigger event.
4036              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
4037          }
4038      }
4039  
4040      return get_complete_user_data('id', $oldinfo->id);
4041  }
4042  
4043  /**
4044   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4045   *
4046   * @param array $info Array of user properties to truncate if needed
4047   * @return array The now truncated information that was passed in
4048   */
4049  function truncate_userinfo(array $info) {
4050      // Define the limits.
4051      $limit = array(
4052          'username'    => 100,
4053          'idnumber'    => 255,
4054          'firstname'   => 100,
4055          'lastname'    => 100,
4056          'email'       => 100,
4057          'phone1'      =>  20,
4058          'phone2'      =>  20,
4059          'institution' => 255,
4060          'department'  => 255,
4061          'address'     => 255,
4062          'city'        => 120,
4063          'country'     =>   2,
4064      );
4065  
4066      // Apply where needed.
4067      foreach (array_keys($info) as $key) {
4068          if (!empty($limit[$key])) {
4069              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4070          }
4071      }
4072  
4073      return $info;
4074  }
4075  
4076  /**
4077   * Marks user deleted in internal user database and notifies the auth plugin.
4078   * Also unenrols user from all roles and does other cleanup.
4079   *
4080   * Any plugin that needs to purge user data should register the 'user_deleted' event.
4081   *
4082   * @param stdClass $user full user object before delete
4083   * @return boolean success
4084   * @throws coding_exception if invalid $user parameter detected
4085   */
4086  function delete_user(stdClass $user) {
4087      global $CFG, $DB, $SESSION;
4088      require_once($CFG->libdir.'/grouplib.php');
4089      require_once($CFG->libdir.'/gradelib.php');
4090      require_once($CFG->dirroot.'/message/lib.php');
4091      require_once($CFG->dirroot.'/user/lib.php');
4092  
4093      // Make sure nobody sends bogus record type as parameter.
4094      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4095          throw new coding_exception('Invalid $user parameter in delete_user() detected');
4096      }
4097  
4098      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4099      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4100          debugging('Attempt to delete unknown user account.');
4101          return false;
4102      }
4103  
4104      // There must be always exactly one guest record, originally the guest account was identified by username only,
4105      // now we use $CFG->siteguest for performance reasons.
4106      if ($user->username === 'guest' or isguestuser($user)) {
4107          debugging('Guest user account can not be deleted.');
4108          return false;
4109      }
4110  
4111      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4112      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4113      if ($user->auth === 'manual' and is_siteadmin($user)) {
4114          debugging('Local administrator accounts can not be deleted.');
4115          return false;
4116      }
4117  
4118      // Allow plugins to use this user object before we completely delete it.
4119      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4120          foreach ($pluginsfunction as $plugintype => $plugins) {
4121              foreach ($plugins as $pluginfunction) {
4122                  $pluginfunction($user);
4123              }
4124          }
4125      }
4126  
4127      // Keep user record before updating it, as we have to pass this to user_deleted event.
4128      $olduser = clone $user;
4129  
4130      // Keep a copy of user context, we need it for event.
4131      $usercontext = context_user::instance($user->id);
4132  
4133      // Delete all grades - backup is kept in grade_grades_history table.
4134      grade_user_delete($user->id);
4135  
4136      // TODO: remove from cohorts using standard API here.
4137  
4138      // Remove user tags.
4139      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4140  
4141      // Unconditionally unenrol from all courses.
4142      enrol_user_delete($user);
4143  
4144      // Unenrol from all roles in all contexts.
4145      // This might be slow but it is really needed - modules might do some extra cleanup!
4146      role_unassign_all(array('userid' => $user->id));
4147  
4148      // Notify the competency subsystem.
4149      \core_competency\api::hook_user_deleted($user->id);
4150  
4151      // Now do a brute force cleanup.
4152  
4153      // Delete all user events and subscription events.
4154      $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4155  
4156      // Now, delete all calendar subscription from the user.
4157      $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4158  
4159      // Remove from all cohorts.
4160      $DB->delete_records('cohort_members', array('userid' => $user->id));
4161  
4162      // Remove from all groups.
4163      $DB->delete_records('groups_members', array('userid' => $user->id));
4164  
4165      // Brute force unenrol from all courses.
4166      $DB->delete_records('user_enrolments', array('userid' => $user->id));
4167  
4168      // Purge user preferences.
4169      $DB->delete_records('user_preferences', array('userid' => $user->id));
4170  
4171      // Purge user extra profile info.
4172      $DB->delete_records('user_info_data', array('userid' => $user->id));
4173  
4174      // Purge log of previous password hashes.
4175      $DB->delete_records('user_password_history', array('userid' => $user->id));
4176  
4177      // Last course access not necessary either.
4178      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4179      // Remove all user tokens.
4180      $DB->delete_records('external_tokens', array('userid' => $user->id));
4181  
4182      // Unauthorise the user for all services.
4183      $DB->delete_records('external_services_users', array('userid' => $user->id));
4184  
4185      // Remove users private keys.
4186      $DB->delete_records('user_private_key', array('userid' => $user->id));
4187  
4188      // Remove users customised pages.
4189      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4190  
4191      // Remove user's oauth2 refresh tokens, if present.
4192      $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
4193  
4194      // Delete user from $SESSION->bulk_users.
4195      if (isset($SESSION->bulk_users[$user->id])) {
4196          unset($SESSION->bulk_users[$user->id]);
4197      }
4198  
4199      // Force logout - may fail if file based sessions used, sorry.
4200      \core\session\manager::kill_user_sessions($user->id);
4201  
4202      // Generate username from email address, or a fake email.
4203      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4204  
4205      $deltime = time();
4206      $deltimelength = core_text::strlen((string) $deltime);
4207  
4208      // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4209      $delname = clean_param($delemail, PARAM_USERNAME);
4210      $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4211  
4212      // Workaround for bulk deletes of users with the same email address.
4213      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4214          $delname++;
4215      }
4216  
4217      // Mark internal user record as "deleted".
4218      $updateuser = new stdClass();
4219      $updateuser->id           = $user->id;
4220      $updateuser->deleted      = 1;
4221      $updateuser->username     = $delname;            // Remember it just in case.
4222      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4223      $updateuser->idnumber     = '';                  // Clear this field to free it up.
4224      $updateuser->picture      = 0;
4225      $updateuser->timemodified = $deltime;
4226  
4227      // Don't trigger update event, as user is being deleted.
4228      user_update_user($updateuser, false, false);
4229  
4230      // Delete all content associated with the user context, but not the context itself.
4231      $usercontext->delete_content();
4232  
4233      // Delete any search data.
4234      \core_search\manager::context_deleted($usercontext);
4235  
4236      // Any plugin that needs to cleanup should register this event.
4237      // Trigger event.
4238      $event = \core\event\user_deleted::create(
4239              array(
4240                  'objectid' => $user->id,
4241                  'relateduserid' => $user->id,
4242                  'context' => $usercontext,
4243                  'other' => array(
4244                      'username' => $user->username,
4245                      'email' => $user->email,
4246                      'idnumber' => $user->idnumber,
4247                      'picture' => $user->picture,
4248                      'mnethostid' => $user->mnethostid
4249                      )
4250                  )
4251              );
4252      $event->add_record_snapshot('user', $olduser);
4253      $event->trigger();
4254  
4255      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4256      // should know about this updated property persisted to the user's table.
4257      $user->timemodified = $updateuser->timemodified;
4258  
4259      // Notify auth plugin - do not block the delete even when plugin fails.
4260      $authplugin = get_auth_plugin($user->auth);
4261      $authplugin->user_delete($user);
4262  
4263      return true;
4264  }
4265  
4266  /**
4267   * Retrieve the guest user object.
4268   *
4269   * @return stdClass A {@link $USER} object
4270   */
4271  function guest_user() {
4272      global $CFG, $DB;
4273  
4274      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4275          $newuser->confirmed = 1;
4276          $newuser->lang = get_newuser_language();
4277          $newuser->lastip = getremoteaddr();
4278      }
4279  
4280      return $newuser;
4281  }
4282  
4283  /**
4284   * Authenticates a user against the chosen authentication mechanism
4285   *
4286   * Given a username and password, this function looks them
4287   * up using the currently selected authentication mechanism,
4288   * and if the authentication is successful, it returns a
4289   * valid $user object from the 'user' table.
4290   *
4291   * Uses auth_ functions from the currently active auth module
4292   *
4293   * After authenticate_user_login() returns success, you will need to
4294   * log that the user has logged in, and call complete_user_login() to set
4295   * the session up.
4296   *
4297   * Note: this function works only with non-mnet accounts!
4298   *
4299   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4300   * @param string $password  User's password
4301   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4302   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4303   * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
4304   * @return stdClass|false A {@link $USER} object or false if error
4305   */
4306  function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
4307      global $CFG, $DB, $PAGE;
4308      require_once("$CFG->libdir/authlib.php");
4309  
4310      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4311          // we have found the user
4312  
4313      } else if (!empty($CFG->authloginviaemail)) {
4314          if ($email = clean_param($username, PARAM_EMAIL)) {
4315              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4316              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4317              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4318              if (count($users) === 1) {
4319                  // Use email for login only if unique.
4320                  $user = reset($users);
4321                  $user = get_complete_user_data('id', $user->id);
4322                  $username = $user->username;
4323              }
4324              unset($users);
4325          }
4326      }
4327  
4328      // Make sure this request came from the login form.
4329      if (!\core\session\manager::validate_login_token($logintoken)) {
4330          $failurereason = AUTH_LOGIN_FAILED;
4331  
4332          // Trigger login failed event (specifying the ID of the found user, if available).
4333          \core\event\user_login_failed::create([
4334              'userid' => ($user->id ?? 0),
4335              'other' => [
4336                  'username' => $username,
4337                  'reason' => $failurereason,
4338              ],
4339          ])->trigger();
4340  
4341          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4342          return false;
4343      }
4344  
4345      $authsenabled = get_enabled_auth_plugins();
4346  
4347      if ($user) {
4348          // Use manual if auth not set.
4349          $auth = empty($user->auth) ? 'manual' : $user->auth;
4350  
4351          if (in_array($user->auth, $authsenabled)) {
4352              $authplugin = get_auth_plugin($user->auth);
4353              $authplugin->pre_user_login_hook($user);
4354          }
4355  
4356          if (!empty($user->suspended)) {
4357              $failurereason = AUTH_LOGIN_SUSPENDED;
4358  
4359              // Trigger login failed event.
4360              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4361                      'other' => array('username' => $username, 'reason' => $failurereason)));
4362              $event->trigger();
4363              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4364              return false;
4365          }
4366          if ($auth=='nologin' or !is_enabled_auth($auth)) {
4367              // Legacy way to suspend user.
4368              $failurereason = AUTH_LOGIN_SUSPENDED;
4369  
4370              // Trigger login failed event.
4371              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4372                      'other' => array('username' => $username, 'reason' => $failurereason)));
4373              $event->trigger();
4374              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4375              return false;
4376          }
4377          $auths = array($auth);
4378  
4379      } else {
4380          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4381          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4382              $failurereason = AUTH_LOGIN_NOUSER;
4383  
4384              // Trigger login failed event.
4385              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4386                      'reason' => $failurereason)));
4387              $event->trigger();
4388              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4389              return false;
4390          }
4391  
4392          // User does not exist.
4393          $auths = $authsenabled;
4394          $user = new stdClass();
4395          $user->id = 0;
4396      }
4397  
4398      if ($ignorelockout) {
4399          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4400          // or this function is called from a SSO script.
4401      } else if ($user->id) {
4402          // Verify login lockout after other ways that may prevent user login.
4403          if (login_is_lockedout($user)) {
4404              $failurereason = AUTH_LOGIN_LOCKOUT;
4405  
4406              // Trigger login failed event.
4407              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4408                      'other' => array('username' => $username, 'reason' => $failurereason)));
4409              $event->trigger();
4410  
4411              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4412              return false;
4413          }
4414      } else {
4415          // We can not lockout non-existing accounts.
4416      }
4417  
4418      foreach ($auths as $auth) {
4419          $authplugin = get_auth_plugin($auth);
4420  
4421          // On auth fail fall through to the next plugin.
4422          if (!$authplugin->user_login($username, $password)) {
4423              continue;
4424          }
4425  
4426          // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4427          if (!empty($CFG->passwordpolicycheckonlogin)) {
4428              $errmsg = '';
4429              $passed = check_password_policy($password, $errmsg, $user);
4430              if (!$passed) {
4431                  // First trigger event for failure.
4432                  $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4433                  $failedevent->trigger();
4434  
4435                  // If able to change password, set flag and move on.
4436                  if ($authplugin->can_change_password()) {
4437                      // Check if we are on internal change password page, or service is external, don't show notification.
4438                      $internalchangeurl = new moodle_url('/login/change_password.php');
4439                      if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4440                          \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4441                      }
4442                      set_user_preference('auth_forcepasswordchange', 1, $user);
4443                  } else if ($authplugin->can_reset_password()) {
4444                      // Else force a reset if possible.
4445                      \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4446                      redirect(new moodle_url('/login/forgot_password.php'));
4447                  } else {
4448                      $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4449                      // If support page is set, add link for help.
4450                      if (!empty($CFG->supportpage)) {
4451                          $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4452                          $link = \html_writer::tag('p', $link);
4453                          $notifymsg .= $link;
4454                      }
4455  
4456                      // If no change or reset is possible, add a notification for user.
4457                      \core\notification::error($notifymsg);
4458                  }
4459              }
4460          }
4461  
4462          // Successful authentication.
4463          if ($user->id) {
4464              // User already exists in database.
4465              if (empty($user->auth)) {
4466                  // For some reason auth isn't set yet.
4467                  $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4468                  $user->auth = $auth;
4469              }
4470  
4471              // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4472              // the current hash algorithm while we have access to the user's password.
4473              update_internal_user_password($user, $password);
4474  
4475              if ($authplugin->is_synchronised_with_external()) {
4476                  // Update user record from external DB.
4477                  $user = update_user_record_by_id($user->id);
4478              }
4479          } else {
4480              // The user is authenticated but user creation may be disabled.
4481              if (!empty($CFG->authpreventaccountcreation)) {
4482                  $failurereason = AUTH_LOGIN_UNAUTHORISED;
4483  
4484                  // Trigger login failed event.
4485                  $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4486                          'reason' => $failurereason)));
4487                  $event->trigger();
4488  
4489                  error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4490                          $_SERVER['HTTP_USER_AGENT']);
4491                  return false;
4492              } else {
4493                  $user = create_user_record($username, $password, $auth);
4494              }
4495          }
4496  
4497          $authplugin->sync_roles($user);
4498  
4499          foreach ($authsenabled as $hau) {
4500              $hauth = get_auth_plugin($hau);
4501              $hauth->user_authenticated_hook($user, $username, $password);
4502          }
4503  
4504          if (empty($user->id)) {
4505              $failurereason = AUTH_LOGIN_NOUSER;
4506              // Trigger login failed event.
4507              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4508                      'reason' => $failurereason)));
4509              $event->trigger();
4510              return false;
4511          }
4512  
4513          if (!empty($user->suspended)) {
4514              // Just in case some auth plugin suspended account.
4515              $failurereason = AUTH_LOGIN_SUSPENDED;
4516              // Trigger login failed event.
4517              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4518                      'other' => array('username' => $username, 'reason' => $failurereason)));
4519              $event->trigger();
4520              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4521              return false;
4522          }
4523  
4524          login_attempt_valid($user);
4525          $failurereason = AUTH_LOGIN_OK;
4526          return $user;
4527      }
4528  
4529      // Failed if all the plugins have failed.
4530      if (debugging('', DEBUG_ALL)) {
4531          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4532      }
4533  
4534      if ($user->id) {
4535          login_attempt_failed($user);
4536          $failurereason = AUTH_LOGIN_FAILED;
4537          // Trigger login failed event.
4538          $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4539                  'other' => array('username' => $username, 'reason' => $failurereason)));
4540          $event->trigger();
4541      } else {
4542          $failurereason = AUTH_LOGIN_NOUSER;
4543          // Trigger login failed event.
4544          $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4545                  'reason' => $failurereason)));
4546          $event->trigger();
4547      }
4548  
4549      return false;
4550  }
4551  
4552  /**
4553   * Call to complete the user login process after authenticate_user_login()
4554   * has succeeded. It will setup the $USER variable and other required bits
4555   * and pieces.
4556   *
4557   * NOTE:
4558   * - It will NOT log anything -- up to the caller to decide what to log.
4559   * - this function does not set any cookies any more!
4560   *
4561   * @param stdClass $user
4562   * @return stdClass A {@link $USER} object - BC only, do not use
4563   */
4564  function complete_user_login($user) {
4565      global $CFG, $DB, $USER, $SESSION;
4566  
4567      \core\session\manager::login_user($user);
4568  
4569      // Reload preferences from DB.
4570      unset($USER->preference);
4571      check_user_preferences_loaded($USER);
4572  
4573      // Update login times.
4574      update_user_login_times();
4575  
4576      // Extra session prefs init.
4577      set_login_session_preferences();
4578  
4579      // Trigger login event.
4580      $event = \core\event\user_loggedin::create(
4581          array(
4582              'userid' => $USER->id,
4583              'objectid' => $USER->id,
4584              'other' => array('username' => $USER->username),
4585          )
4586      );
4587      $event->trigger();
4588  
4589      // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4590      // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4591      // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4592      $loginip = getremoteaddr();
4593      $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4594      $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4595  
4596      if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4597  
4598          $logintime = time();
4599          $ismoodleapp = false;
4600          $useragent = \core_useragent::get_user_agent_string();
4601  
4602          // Schedule adhoc task to sent a login notification to the user.
4603          $task = new \core\task\send_login_notifications();
4604          $task->set_userid($USER->id);
4605          $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4606          $task->set_component('core');
4607          \core\task\manager::queue_adhoc_task($task);
4608      }
4609  
4610      // Queue migrating the messaging data, if we need to.
4611      if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4612          // Check if there are any legacy messages to migrate.
4613          if (\core_message\helper::legacy_messages_exist($USER->id)) {
4614              \core_message\task\migrate_message_data::queue_task($USER->id);
4615          } else {
4616              set_user_preference('core_message_migrate_data', true, $USER->id);
4617          }
4618      }
4619  
4620      if (isguestuser()) {
4621          // No need to continue when user is THE guest.
4622          return $USER;
4623      }
4624  
4625      if (CLI_SCRIPT) {
4626          // We can redirect to password change URL only in browser.
4627          return $USER;
4628      }
4629  
4630      // Select password change url.
4631      $userauth = get_auth_plugin($USER->auth);
4632  
4633      // Check whether the user should be changing password.
4634      if (get_user_preferences('auth_forcepasswordchange', false)) {
4635          if ($userauth->can_change_password()) {
4636              if ($changeurl = $userauth->change_password_url()) {
4637                  redirect($changeurl);
4638              } else {
4639                  require_once($CFG->dirroot . '/login/lib.php');
4640                  $SESSION->wantsurl = core_login_get_return_url();
4641                  redirect($CFG->wwwroot.'/login/change_password.php');
4642              }
4643          } else {
4644              print_error('nopasswordchangeforced', 'auth');
4645          }
4646      }
4647      return $USER;
4648  }
4649  
4650  /**
4651   * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4652   *
4653   * @param string $password String to check.
4654   * @return boolean True if the $password matches the format of an md5 sum.
4655   */
4656  function password_is_legacy_hash($password) {
4657      return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4658  }
4659  
4660  /**
4661   * Compare password against hash stored in user object to determine if it is valid.
4662   *
4663   * If necessary it also updates the stored hash to the current format.
4664   *
4665   * @param stdClass $user (Password property may be updated).
4666   * @param string $password Plain text password.
4667   * @return bool True if password is valid.
4668   */
4669  function validate_internal_user_password($user, $password) {
4670      global $CFG;
4671  
4672      if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4673          // Internal password is not used at all, it can not validate.
4674          return false;
4675      }
4676  
4677      // If hash isn't a legacy (md5) hash, validate using the library function.
4678      if (!password_is_legacy_hash($user->password)) {
4679          return password_verify($password, $user->password);
4680      }
4681  
4682      // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4683      // is valid we can then update it to the new algorithm.
4684  
4685      $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4686      $validated = false;
4687  
4688      if ($user->password === md5($password.$sitesalt)
4689              or $user->password === md5($password)
4690              or $user->password === md5(addslashes($password).$sitesalt)
4691              or $user->password === md5(addslashes($password))) {
4692          // Note: we are intentionally using the addslashes() here because we
4693          //       need to accept old password hashes of passwords with magic quotes.
4694          $validated = true;
4695  
4696      } else {
4697          for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4698              $alt = 'passwordsaltalt'.$i;
4699              if (!empty($CFG->$alt)) {
4700                  if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4701                      $validated = true;
4702                      break;
4703                  }
4704              }
4705          }
4706      }
4707  
4708      if ($validated) {
4709          // If the password matches the existing md5 hash, update to the
4710          // current hash algorithm while we have access to the user's password.
4711          update_internal_user_password($user, $password);
4712      }
4713  
4714      return $validated;
4715  }
4716  
4717  /**
4718   * Calculate hash for a plain text password.
4719   *
4720   * @param string $password Plain text password to be hashed.
4721   * @param bool $fasthash If true, use a low cost factor when generating the hash
4722   *                       This is much faster to generate but makes the hash
4723   *                       less secure. It is used when lots of hashes need to
4724   *                       be generated quickly.
4725   * @return string The hashed password.
4726   *
4727   * @throws moodle_exception If a problem occurs while generating the hash.
4728   */
4729  function hash_internal_user_password($password, $fasthash = false) {
4730      global $CFG;
4731  
4732      // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4733      $options = ($fasthash) ? array('cost' => 4) : array();
4734  
4735      $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4736  
4737      if ($generatedhash === false || $generatedhash === null) {
4738          throw new moodle_exception('Failed to generate password hash.');
4739      }
4740  
4741      return $generatedhash;
4742  }
4743  
4744  /**
4745   * Update password hash in user object (if necessary).
4746   *
4747   * The password is updated if:
4748   * 1. The password has changed (the hash of $user->password is different
4749   *    to the hash of $password).
4750   * 2. The existing hash is using an out-of-date algorithm (or the legacy
4751   *    md5 algorithm).
4752   *
4753   * Updating the password will modify the $user object and the database
4754   * record to use the current hashing algorithm.
4755   * It will remove Web Services user tokens too.
4756   *
4757   * @param stdClass $user User object (password property may be updated).
4758   * @param string $password Plain text password.
4759   * @param bool $fasthash If true, use a low cost factor when generating the hash
4760   *                       This is much faster to generate but makes the hash
4761   *                       less secure. It is used when lots of hashes need to
4762   *                       be generated quickly.
4763   * @return bool Always returns true.
4764   */
4765  function update_internal_user_password($user, $password, $fasthash = false) {
4766      global $CFG, $DB;
4767  
4768      // Figure out what the hashed password should be.
4769      if (!isset($user->auth)) {
4770          debugging('User record in update_internal_user_password() must include field auth',
4771                  DEBUG_DEVELOPER);
4772          $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4773      }
4774      $authplugin = get_auth_plugin($user->auth);
4775      if ($authplugin->prevent_local_passwords()) {
4776          $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4777      } else {
4778          $hashedpassword = hash_internal_user_password($password, $fasthash);
4779      }
4780  
4781      $algorithmchanged = false;
4782  
4783      if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4784          // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4785          $passwordchanged = ($user->password !== $hashedpassword);
4786  
4787      } else if (isset($user->password)) {
4788          // If verification fails then it means the password has changed.
4789          $passwordchanged = !password_verify($password, $user->password);
4790          $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4791      } else {
4792          // While creating new user, password in unset in $user object, to avoid
4793          // saving it with user_create()
4794          $passwordchanged = true;
4795      }
4796  
4797      if ($passwordchanged || $algorithmchanged) {
4798          $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4799          $user->password = $hashedpassword;
4800  
4801          // Trigger event.
4802          $user = $DB->get_record('user', array('id' => $user->id));
4803          \core\event\user_password_updated::create_from_user($user)->trigger();
4804  
4805          // Remove WS user tokens.
4806          if (!empty($CFG->passwordchangetokendeletion)) {
4807              require_once($CFG->dirroot.'/webservice/lib.php');
4808              webservice::delete_user_ws_tokens($user->id);
4809          }
4810      }
4811  
4812      return true;
4813  }
4814  
4815  /**
4816   * Get a complete user record, which includes all the info in the user record.
4817   *
4818   * Intended for setting as $USER session variable
4819   *
4820   * @param string $field The user field to be checked for a given value.
4821   * @param string $value The value to match for $field.
4822   * @param int $mnethostid
4823   * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4824   *                              found. Otherwise, it will just return false.
4825   * @return mixed False, or A {@link $USER} object.
4826   */
4827  function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4828      global $CFG, $DB;
4829  
4830      if (!$field || !$value) {
4831          return false;
4832      }
4833  
4834      // Change the field to lowercase.
4835      $field = core_text::strtolower($field);
4836  
4837      // List of case insensitive fields.
4838      $caseinsensitivefields = ['email'];
4839  
4840      // Username input is forced to lowercase and should be case sensitive.
4841      if ($field == 'username') {
4842          $value = core_text::strtolower($value);
4843      }
4844  
4845      // Build the WHERE clause for an SQL query.
4846      $params = array('fieldval' => $value);
4847  
4848      // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4849      // such as MySQL by pre-filtering users with accent-insensitive subselect.
4850      if (in_array($field, $caseinsensitivefields)) {
4851          $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4852          $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4853          $params['fieldval2'] = $value;
4854      } else {
4855          $fieldselect = "$field = :fieldval";
4856          $idsubselect = '';
4857      }
4858      $constraints = "$fieldselect AND deleted <> 1";
4859  
4860      // If we are loading user data based on anything other than id,
4861      // we must also restrict our search based on mnet host.
4862      if ($field != 'id') {
4863          if (empty($mnethostid)) {
4864              // If empty, we restrict to local users.
4865              $mnethostid = $CFG->mnet_localhost_id;
4866          }
4867      }
4868      if (!empty($mnethostid)) {
4869          $params['mnethostid'] = $mnethostid;
4870          $constraints .= " AND mnethostid = :mnethostid";
4871      }
4872  
4873      if ($idsubselect) {
4874          $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4875      }
4876  
4877      // Get all the basic user data.
4878      try {
4879          // Make sure that there's only a single record that matches our query.
4880          // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4881          // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4882          $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4883      } catch (dml_exception $exception) {
4884          if ($throwexception) {
4885              throw $exception;
4886          } else {
4887              // Return false when no records or multiple records were found.
4888              return false;
4889          }
4890      }
4891  
4892      // Get various settings and preferences.
4893  
4894      // Preload preference cache.
4895      check_user_preferences_loaded($user);
4896  
4897      // Load course enrolment related stuff.
4898      $user->lastcourseaccess    = array(); // During last session.
4899      $user->currentcourseaccess = array(); // During current session.
4900      if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4901          foreach ($lastaccesses as $lastaccess) {
4902              $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4903          }
4904      }
4905  
4906      $sql = "SELECT g.id, g.courseid
4907                FROM {groups} g, {groups_members} gm
4908               WHERE gm.groupid=g.id AND gm.userid=?";
4909  
4910      // This is a special hack to speedup calendar display.
4911      $user->groupmember = array();
4912      if (!isguestuser($user)) {
4913          if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4914              foreach ($groups as $group) {
4915                  if (!array_key_exists($group->courseid, $user->groupmember)) {
4916                      $user->groupmember[$group->courseid] = array();
4917                  }
4918                  $user->groupmember[$group->courseid][$group->id] = $group->id;
4919              }
4920          }
4921      }
4922  
4923      // Add cohort theme.
4924      if (!empty($CFG->allowcohortthemes)) {
4925          require_once($CFG->dirroot . '/cohort/lib.php');
4926          if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4927              $user->cohorttheme = $cohorttheme;
4928          }
4929      }
4930  
4931      // Add the custom profile fields to the user record.
4932      $user->profile = array();
4933      if (!isguestuser($user)) {
4934          require_once($CFG->dirroot.'/user/profile/lib.php');
4935          profile_load_custom_fields($user);
4936      }
4937  
4938      // Rewrite some variables if necessary.
4939      if (!empty($user->description)) {
4940          // No need to cart all of it around.
4941          $user->description = true;
4942      }
4943      if (isguestuser($user)) {
4944          // Guest language always same as site.
4945          $user->lang = get_newuser_language();
4946          // Name always in current language.
4947          $user->firstname = get_string('guestuser');
4948          $user->lastname = ' ';
4949      }
4950  
4951      return $user;
4952  }
4953  
4954  /**
4955   * Validate a password against the configured password policy
4956   *
4957   * @param string $password the password to be checked against the password policy
4958   * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4959   * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
4960   *
4961   * @return bool true if the password is valid according to the policy. false otherwise.
4962   */
4963  function check_password_policy($password, &$errmsg, $user = null) {
4964      global $CFG;
4965  
4966      if (!empty($CFG->passwordpolicy)) {
4967          $errmsg = '';
4968          if (core_text::strlen($password) < $CFG->minpasswordlength) {
4969              $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4970          }
4971          if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4972              $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4973          }
4974          if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4975              $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4976          }
4977          if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4978              $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4979          }
4980          if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4981              $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4982          }
4983          if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4984              $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4985          }
4986  
4987          // Fire any additional password policy functions from plugins.
4988          // Plugin functions should output an error message string or empty string for success.
4989          $pluginsfunction = get_plugins_with_function('check_password_policy');
4990          foreach ($pluginsfunction as $plugintype => $plugins) {
4991              foreach ($plugins as $pluginfunction) {
4992                  $pluginerr = $pluginfunction($password, $user);
4993                  if ($pluginerr) {
4994                      $errmsg .= '<div>'. $pluginerr .'</div>';
4995                  }
4996              }
4997          }
4998      }
4999  
5000      if ($errmsg == '') {
5001          return true;
5002      } else {
5003          return false;
5004      }
5005  }
5006  
5007  
5008  /**
5009   * When logging in, this function is run to set certain preferences for the current SESSION.
5010   */
5011  function set_login_session_preferences() {
5012      global $SESSION;
5013  
5014      $SESSION->justloggedin = true;
5015  
5016      unset($SESSION->lang);
5017      unset($SESSION->forcelang);
5018      unset($SESSION->load_navigation_admin);
5019  }
5020  
5021  
5022  /**
5023   * Delete a course, including all related data from the database, and any associated files.
5024   *
5025   * @param mixed $courseorid The id of the course or course object to delete.
5026   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5027   * @return bool true if all the removals succeeded. false if there were any failures. If this
5028   *             method returns false, some of the removals will probably have succeeded, and others
5029   *             failed, but you have no way of knowing which.
5030   */
5031  function delete_course($courseorid, $showfeedback = true) {
5032      global $DB;
5033  
5034      if (is_object($courseorid)) {
5035          $courseid = $courseorid->id;
5036          $course   = $courseorid;
5037      } else {
5038          $courseid = $courseorid;
5039          if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5040              return false;
5041          }
5042      }
5043      $context = context_course::instance($courseid);
5044  
5045      // Frontpage course can not be deleted!!
5046      if ($courseid == SITEID) {
5047          return false;
5048      }
5049  
5050      // Allow plugins to use this course before we completely delete it.
5051      if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5052          foreach ($pluginsfunction as $plugintype => $plugins) {
5053              foreach ($plugins as $pluginfunction) {
5054                  $pluginfunction($course);
5055              }
5056          }
5057      }
5058  
5059      // Tell the search manager we are about to delete a course. This prevents us sending updates
5060      // for each individual context being deleted.
5061      \core_search\manager::course_deleting_start($courseid);
5062  
5063      $handler = core_course\customfield\course_handler::create();
5064      $handler->delete_instance($courseid);
5065  
5066      // Make the course completely empty.
5067      remove_course_contents($courseid, $showfeedback);
5068  
5069      // Delete the course and related context instance.
5070      context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5071  
5072      $DB->delete_records("course", array("id" => $courseid));
5073      $DB->delete_records("course_format_options", array("courseid" => $courseid));
5074  
5075      // Reset all course related caches here.
5076      core_courseformat\base::reset_course_cache($courseid);
5077  
5078      // Tell search that we have deleted the course so it can delete course data from the index.
5079      \core_search\manager::course_deleting_finish($courseid);
5080  
5081      // Trigger a course deleted event.
5082      $event = \core\event\course_deleted::create(array(
5083          'objectid' => $course->id,
5084          'context' => $context,
5085          'other' => array(
5086              'shortname' => $course->shortname,
5087              'fullname' => $course->fullname,
5088              'idnumber' => $course->idnumber
5089              )
5090      ));
5091      $event->add_record_snapshot('course', $course);
5092      $event->trigger();
5093  
5094      return true;
5095  }
5096  
5097  /**
5098   * Clear a course out completely, deleting all content but don't delete the course itself.
5099   *
5100   * This function does not verify any permissions.
5101   *
5102   * Please note this function also deletes all user enrolments,
5103   * enrolment instances and role assignments by default.
5104   *
5105   * $options:
5106   *  - 'keep_roles_and_enrolments' - false by default
5107   *  - 'keep_groups_and_groupings' - false by default
5108   *
5109   * @param int $courseid The id of the course that is being deleted
5110   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5111   * @param array $options extra options
5112   * @return bool true if all the removals succeeded. false if there were any failures. If this
5113   *             method returns false, some of the removals will probably have succeeded, and others
5114   *             failed, but you have no way of knowing which.
5115   */
5116  function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5117      global $CFG, $DB, $OUTPUT;
5118  
5119      require_once($CFG->libdir.'/badgeslib.php');
5120      require_once($CFG->libdir.'/completionlib.php');
5121      require_once($CFG->libdir.'/questionlib.php');
5122      require_once($CFG->libdir.'/gradelib.php');
5123      require_once($CFG->dirroot.'/group/lib.php');
5124      require_once($CFG->dirroot.'/comment/lib.php');
5125      require_once($CFG->dirroot.'/rating/lib.php');
5126      require_once($CFG->dirroot.'/notes/lib.php');
5127  
5128      // Handle course badges.
5129      badges_handle_course_deletion($courseid);
5130  
5131      // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5132      $strdeleted = get_string('deleted').' - ';
5133  
5134      // Some crazy wishlist of stuff we should skip during purging of course content.
5135      $options = (array)$options;
5136  
5137      $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5138      $coursecontext = context_course::instance($courseid);
5139      $fs = get_file_storage();
5140  
5141      // Delete course completion information, this has to be done before grades and enrols.
5142      $cc = new completion_info($course);
5143      $cc->clear_criteria();
5144      if ($showfeedback) {
5145          echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5146      }
5147  
5148      // Remove all data from gradebook - this needs to be done before course modules
5149      // because while deleting this information, the system may need to reference
5150      // the course modules that own the grades.
5151      remove_course_grades($courseid, $showfeedback);
5152      remove_grade_letters($coursecontext, $showfeedback);
5153  
5154      // Delete course blocks in any all child contexts,
5155      // they may depend on modules so delete them first.
5156      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5157      foreach ($childcontexts as $childcontext) {
5158          blocks_delete_all_for_context($childcontext->id);
5159      }
5160      unset($childcontexts);
5161      blocks_delete_all_for_context($coursecontext->id);
5162      if ($showfeedback) {
5163          echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5164      }
5165  
5166      $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5167      rebuild_course_cache($courseid, true);
5168  
5169      // Get the list of all modules that are properly installed.
5170      $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5171  
5172      // Delete every instance of every module,
5173      // this has to be done before deleting of course level stuff.
5174      $locations = core_component::get_plugin_list('mod');
5175      foreach ($locations as $modname => $moddir) {
5176          if ($modname === 'NEWMODULE') {
5177              continue;
5178          }
5179          if (array_key_exists($modname, $allmodules)) {
5180              $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5181                FROM {".$modname."} m
5182                     LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5183               WHERE m.course = :courseid";
5184              $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5185                  'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5186  
5187              include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
5188              $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
5189  
5190              if ($instances) {
5191                  foreach ($instances as $cm) {
5192                      if ($cm->id) {
5193                          // Delete activity context questions and question categories.
5194                          question_delete_activity($cm);
5195                          // Notify the competency subsystem.
5196                          \core_competency\api::hook_course_module_deleted($cm);
5197  
5198                          // Delete all tag instances associated with the instance of this module.
5199                          core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
5200                          core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
5201                      }
5202                      if (function_exists($moddelete)) {
5203                          // This purges all module data in related tables, extra user prefs, settings, etc.
5204                          $moddelete($cm->modinstance);
5205                      } else {
5206                          // NOTE: we should not allow installation of modules with missing delete support!
5207                          debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5208                          $DB->delete_records($modname, array('id' => $cm->modinstance));
5209                      }
5210  
5211                      if ($cm->id) {
5212                          // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5213                          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5214                          $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5215                          $DB->delete_records('course_modules', array('id' => $cm->id));
5216                          rebuild_course_cache($cm->course, true);
5217                      }
5218                  }
5219              }
5220              if ($instances and $showfeedback) {
5221                  echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5222              }
5223          } else {
5224              // Ooops, this module is not properly installed, force-delete it in the next block.
5225          }
5226      }
5227  
5228      // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5229  
5230      // Delete completion defaults.
5231      $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5232  
5233      // Remove all data from availability and completion tables that is associated
5234      // with course-modules belonging to this course. Note this is done even if the
5235      // features are not enabled now, in case they were enabled previously.
5236      $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5237              'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5238  
5239      // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5240      $cms = $DB->get_records('course_modules', array('course' => $course->id));
5241      $allmodulesbyid = array_flip($allmodules);
5242      foreach ($cms as $cm) {
5243          if (array_key_exists($cm->module, $allmodulesbyid)) {
5244              try {
5245                  $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5246              } catch (Exception $e) {
5247                  // Ignore weird or missing table problems.
5248              }
5249          }
5250          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5251          $DB->delete_records('course_modules', array('id' => $cm->id));
5252          rebuild_course_cache($cm->course, true);
5253      }
5254  
5255      if ($showfeedback) {
5256          echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5257      }
5258  
5259      // Delete questions and question categories.
5260      question_delete_course($course);
5261      if ($showfeedback) {
5262          echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5263      }
5264  
5265      // Delete content bank contents.
5266      $cb = new \core_contentbank\contentbank();
5267      $cbdeleted = $cb->delete_contents($coursecontext);
5268      if ($showfeedback && $cbdeleted) {
5269          echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5270      }
5271  
5272      // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5273      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5274      foreach ($childcontexts as $childcontext) {
5275          $childcontext->delete();
5276      }
5277      unset($childcontexts);
5278  
5279      // Remove roles and enrolments by default.
5280      if (empty($options['keep_roles_and_enrolments'])) {
5281          // This hack is used in restore when deleting contents of existing course.
5282          // During restore, we should remove only enrolment related data that the user performing the restore has a
5283          // permission to remove.
5284          $userid = $options['userid'] ?? null;
5285          enrol_course_delete($course, $userid);
5286          role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5287          if ($showfeedback) {
5288              echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5289          }
5290      }
5291  
5292      // Delete any groups, removing members and grouping/course links first.
5293      if (empty($options['keep_groups_and_groupings'])) {
5294          groups_delete_groupings($course->id, $showfeedback);
5295          groups_delete_groups($course->id, $showfeedback);
5296      }
5297  
5298      // Filters be gone!
5299      filter_delete_all_for_context($coursecontext->id);
5300  
5301      // Notes, you shall not pass!
5302      note_delete_all($course->id);
5303  
5304      // Die comments!
5305      comment::delete_comments($coursecontext->id);
5306  
5307      // Ratings are history too.
5308      $delopt = new stdclass();
5309      $delopt->contextid = $coursecontext->id;
5310      $rm = new rating_manager();
5311      $rm->delete_ratings($delopt);
5312  
5313      // Delete course tags.
5314      core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5315  
5316      // Give the course format the opportunity to remove its obscure data.
5317      $format = course_get_format($course);
5318      $format->delete_format_data();
5319  
5320      // Notify the competency subsystem.
5321      \core_competency\api::hook_course_deleted($course);
5322  
5323      // Delete calendar events.
5324      $DB->delete_records('event', array('courseid' => $course->id));
5325      $fs->delete_area_files($coursecontext->id, 'calendar');
5326  
5327      // Delete all related records in other core tables that may have a courseid
5328      // This array stores the tables that need to be cleared, as
5329      // table_name => column_name that contains the course id.
5330      $tablestoclear = array(
5331          'backup_courses' => 'courseid',  // Scheduled backup stuff.
5332          'user_lastaccess' => 'courseid', // User access info.
5333      );
5334      foreach ($tablestoclear as $table => $col) {
5335          $DB->delete_records($table, array($col => $course->id));
5336      }
5337  
5338      // Delete all course backup files.
5339      $fs->delete_area_files($coursecontext->id, 'backup');
5340  
5341      // Cleanup course record - remove links to deleted stuff.
5342      $oldcourse = new stdClass();
5343      $oldcourse->id               = $course->id;
5344      $oldcourse->summary          = '';
5345      $oldcourse->cacherev         = 0;
5346      $oldcourse->legacyfiles      = 0;
5347      if (!empty($options['keep_groups_and_groupings'])) {
5348          $oldcourse->defaultgroupingid = 0;
5349      }
5350      $DB->update_record('course', $oldcourse);
5351  
5352      // Delete course sections.
5353      $DB->delete_records('course_sections', array('course' => $course->id));
5354  
5355      // Delete legacy, section and any other course files.
5356      $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5357  
5358      // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5359      if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5360          // Easy, do not delete the context itself...
5361          $coursecontext->delete_content();
5362      } else {
5363          // Hack alert!!!!
5364          // We can not drop all context stuff because it would bork enrolments and roles,
5365          // there might be also files used by enrol plugins...
5366      }
5367  
5368      // Delete legacy files - just in case some files are still left there after conversion to new file api,
5369      // also some non-standard unsupported plugins may try to store something there.
5370      fulldelete($CFG->dataroot.'/'.$course->id);
5371  
5372      // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5373      course_modinfo::purge_course_cache($courseid);
5374  
5375      // Trigger a course content deleted event.
5376      $event = \core\event\course_content_deleted::create(array(
5377          'objectid' => $course->id,
5378          'context' => $coursecontext,
5379          'other' => array('shortname' => $course->shortname,
5380                           'fullname' => $course->fullname,
5381                           'options' => $options) // Passing this for legacy reasons.
5382      ));
5383      $event->add_record_snapshot('course', $course);
5384      $event->trigger();
5385  
5386      return true;
5387  }
5388  
5389  /**
5390   * Change dates in module - used from course reset.
5391   *
5392   * @param string $modname forum, assignment, etc
5393   * @param array $fields array of date fields from mod table
5394   * @param int $timeshift time difference
5395   * @param int $courseid
5396   * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5397   * @return bool success
5398   */
5399  function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5400      global $CFG, $DB;
5401      include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5402  
5403      $return = true;
5404      $params = array($timeshift, $courseid);
5405      foreach ($fields as $field) {
5406          $updatesql = "UPDATE {".$modname."}
5407                            SET $field = $field + ?
5408                          WHERE course=? AND $field<>0";
5409          if ($modid) {
5410              $updatesql .= ' AND id=?';
5411              $params[] = $modid;
5412          }
5413          $return = $DB->execute($updatesql, $params) && $return;
5414      }
5415  
5416      return $return;
5417  }
5418  
5419  /**
5420   * This function will empty a course of user data.
5421   * It will retain the activities and the structure of the course.
5422   *
5423   * @param object $data an object containing all the settings including courseid (without magic quotes)
5424   * @return array status array of array component, item, error
5425   */
5426  function reset_course_userdata($data) {
5427      global $CFG, $DB;
5428      require_once($CFG->libdir.'/gradelib.php');
5429      require_once($CFG->libdir.'/completionlib.php');
5430      require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5431      require_once($CFG->dirroot.'/group/lib.php');
5432  
5433      $data->courseid = $data->id;
5434      $context = context_course::instance($data->courseid);
5435  
5436      $eventparams = array(
5437          'context' => $context,
5438          'courseid' => $data->id,
5439          'other' => array(
5440              'reset_options' => (array) $data
5441          )
5442      );
5443      $event = \core\event\course_reset_started::create($eventparams);
5444      $event->trigger();
5445  
5446      // Calculate the time shift of dates.
5447      if (!empty($data->reset_start_date)) {
5448          // Time part of course startdate should be zero.
5449          $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5450      } else {
5451          $data->timeshift = 0;
5452      }
5453  
5454      // Result array: component, item, error.
5455      $status = array();
5456  
5457      // Start the resetting.
5458      $componentstr = get_string('general');
5459  
5460      // Move the course start time.
5461      if (!empty($data->reset_start_date) and $data->timeshift) {
5462          // Change course start data.
5463          $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5464          // Update all course and group events - do not move activity events.
5465          $updatesql = "UPDATE {event}
5466                           SET timestart = timestart + ?
5467                         WHERE courseid=? AND instance=0";
5468          $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5469  
5470          // Update any date activity restrictions.
5471          if ($CFG->enableavailability) {
5472              \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5473          }
5474  
5475          // Update completion expected dates.
5476          if ($CFG->enablecompletion) {
5477              $modinfo = get_fast_modinfo($data->courseid);
5478              $changed = false;
5479              foreach ($modinfo->get_cms() as $cm) {
5480                  if ($cm->completion && !empty($cm->completionexpected)) {
5481                      $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5482                          array('id' => $cm->id));
5483                      $changed = true;
5484                  }
5485              }
5486  
5487              // Clear course cache if changes made.
5488              if ($changed) {
5489                  rebuild_course_cache($data->courseid, true);
5490              }
5491  
5492              // Update course date completion criteria.
5493              \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5494          }
5495  
5496          $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5497      }
5498  
5499      if (!empty($data->reset_end_date)) {
5500          // If the user set a end date value respect it.
5501          $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5502      } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5503          // If there is a time shift apply it to the end date as well.
5504          $enddate = $data->reset_end_date_old + $data->timeshift;
5505          $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5506      }
5507  
5508      if (!empty($data->reset_events)) {
5509          $DB->delete_records('event', array('courseid' => $data->courseid));
5510          $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5511      }
5512  
5513      if (!empty($data->reset_notes)) {
5514          require_once($CFG->dirroot.'/notes/lib.php');
5515          note_delete_all($data->courseid);
5516          $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5517      }
5518  
5519      if (!empty($data->delete_blog_associations)) {
5520          require_once($CFG->dirroot.'/blog/lib.php');
5521          blog_remove_associations_for_course($data->courseid);
5522          $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5523      }
5524  
5525      if (!empty($data->reset_completion)) {
5526          // Delete course and activity completion information.
5527          $course = $DB->get_record('course', array('id' => $data->courseid));
5528          $cc = new completion_info($course);
5529          $cc->delete_all_completion_data();
5530          $status[] = array('component' => $componentstr,
5531                  'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5532      }
5533  
5534      if (!empty($data->reset_competency_ratings)) {
5535          \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5536          $status[] = array('component' => $componentstr,
5537              'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5538      }
5539  
5540      $componentstr = get_string('roles');
5541  
5542      if (!empty($data->reset_roles_overrides)) {
5543          $children = $context->get_child_contexts();
5544          foreach ($children as $child) {
5545              $child->delete_capabilities();
5546          }
5547          $context->delete_capabilities();
5548          $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5549      }
5550  
5551      if (!empty($data->reset_roles_local)) {
5552          $children = $context->get_child_contexts();
5553          foreach ($children as $child) {
5554              role_unassign_all(array('contextid' => $child->id));
5555          }
5556          $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5557      }
5558  
5559      // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5560      $data->unenrolled = array();
5561      if (!empty($data->unenrol_users)) {
5562          $plugins = enrol_get_plugins(true);
5563          $instances = enrol_get_instances($data->courseid, true);
5564          foreach ($instances as $key => $instance) {
5565              if (!isset($plugins[$instance->enrol])) {
5566                  unset($instances[$key]);
5567                  continue;
5568              }
5569          }
5570  
5571          $usersroles = enrol_get_course_users_roles($data->courseid);
5572          foreach ($data->unenrol_users as $withroleid) {
5573              if ($withroleid) {
5574                  $sql = "SELECT ue.*
5575                            FROM {user_enrolments} ue
5576                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5577                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5578                            JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5579                  $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5580  
5581              } else {
5582                  // Without any role assigned at course context.
5583                  $sql = "SELECT ue.*
5584                            FROM {user_enrolments} ue
5585                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5586                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5587                       LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5588                           WHERE ra.id IS null";
5589                  $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5590              }
5591  
5592              $rs = $DB->get_recordset_sql($sql, $params);
5593              foreach ($rs as $ue) {
5594                  if (!isset($instances[$ue->enrolid])) {
5595                      continue;
5596                  }
5597                  $instance = $instances[$ue->enrolid];
5598                  $plugin = $plugins[$instance->enrol];
5599                  if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5600                      continue;
5601                  }
5602  
5603                  if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5604                      // If we don't remove all roles and user has more than one role, just remove this role.
5605                      role_unassign($withroleid, $ue->userid, $context->id);
5606  
5607                      unset($usersroles[$ue->userid][$withroleid]);
5608                  } else {
5609                      // If we remove all roles or user has only one role, unenrol user from course.
5610                      $plugin->unenrol_user($instance, $ue->userid);
5611                  }
5612                  $data->unenrolled[$ue->userid] = $ue->userid;
5613              }
5614              $rs->close();
5615          }
5616      }
5617      if (!empty($data->unenrolled)) {
5618          $status[] = array(
5619              'component' => $componentstr,
5620              'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5621              'error' => false
5622          );
5623      }
5624  
5625      $componentstr = get_string('groups');
5626  
5627      // Remove all group members.
5628      if (!empty($data->reset_groups_members)) {
5629          groups_delete_group_members($data->courseid);
5630          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5631      }
5632  
5633      // Remove all groups.
5634      if (!empty($data->reset_groups_remove)) {
5635          groups_delete_groups($data->courseid, false);
5636          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5637      }
5638  
5639      // Remove all grouping members.
5640      if (!empty($data->reset_groupings_members)) {
5641          groups_delete_groupings_groups($data->courseid, false);
5642          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5643      }
5644  
5645      // Remove all groupings.
5646      if (!empty($data->reset_groupings_remove)) {
5647          groups_delete_groupings($data->courseid, false);
5648          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5649      }
5650  
5651      // Look in every instance of every module for data to delete.
5652      $unsupportedmods = array();
5653      if ($allmods = $DB->get_records('modules') ) {
5654          foreach ($allmods as $mod) {
5655              $modname = $mod->name;
5656              $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5657              $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5658              if (file_exists($modfile)) {
5659                  if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5660                      continue; // Skip mods with no instances.
5661                  }
5662                  include_once($modfile);
5663                  if (function_exists($moddeleteuserdata)) {
5664                      $modstatus = $moddeleteuserdata($data);
5665                      if (is_array($modstatus)) {
5666                          $status = array_merge($status, $modstatus);
5667                      } else {
5668                          debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5669                      }
5670                  } else {
5671                      $unsupportedmods[] = $mod;
5672                  }
5673              } else {
5674                  debugging('Missing lib.php in '.$modname.' module!');
5675              }
5676              // Update calendar events for all modules.
5677              course_module_bulk_update_calendar_events($modname, $data->courseid);
5678          }
5679          // Purge the course cache after resetting course start date. MDL-76936
5680          if ($data->timeshift) {
5681              course_modinfo::purge_course_cache($data->courseid);
5682          }
5683      }
5684  
5685      // Mention unsupported mods.
5686      if (!empty($unsupportedmods)) {
5687          foreach ($unsupportedmods as $mod) {
5688              $status[] = array(
5689                  'component' => get_string('modulenameplural', $mod->name),
5690                  'item' => '',
5691                  'error' => get_string('resetnotimplemented')
5692              );
5693          }
5694      }
5695  
5696      $componentstr = get_string('gradebook', 'grades');
5697      // Reset gradebook,.
5698      if (!empty($data->reset_gradebook_items)) {
5699          remove_course_grades($data->courseid, false);
5700          grade_grab_course_grades($data->courseid);
5701          grade_regrade_final_grades($data->courseid);
5702          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5703  
5704      } else if (!empty($data->reset_gradebook_grades)) {
5705          grade_course_reset($data->courseid);
5706          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5707      }
5708      // Reset comments.
5709      if (!empty($data->reset_comments)) {
5710          require_once($CFG->dirroot.'/comment/lib.php');
5711          comment::reset_course_page_comments($context);
5712      }
5713  
5714      $event = \core\event\course_reset_ended::create($eventparams);
5715      $event->trigger();
5716  
5717      return $status;
5718  }
5719  
5720  /**
5721   * Generate an email processing address.
5722   *
5723   * @param int $modid
5724   * @param string $modargs
5725   * @return string Returns email processing address
5726   */
5727  function generate_email_processing_address($modid, $modargs) {
5728      global $CFG;
5729  
5730      $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5731      return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5732  }
5733  
5734  /**
5735   * ?
5736   *
5737   * @todo Finish documenting this function
5738   *
5739   * @param string $modargs
5740   * @param string $body Currently unused
5741   */
5742  function moodle_process_email($modargs, $body) {
5743      global $DB;
5744  
5745      // The first char should be an unencoded letter. We'll take this as an action.
5746      switch ($modargs[0]) {
5747          case 'B': { // Bounce.
5748              list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5749              if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5750                  // Check the half md5 of their email.
5751                  $md5check = substr(md5($user->email), 0, 16);
5752                  if ($md5check == substr($modargs, -16)) {
5753                      set_bounce_count($user);
5754                  }
5755                  // Else maybe they've already changed it?
5756              }
5757          }
5758          break;
5759          // Maybe more later?
5760      }
5761  }
5762  
5763  // CORRESPONDENCE.
5764  
5765  /**
5766   * Get mailer instance, enable buffering, flush buffer or disable buffering.
5767   *
5768   * @param string $action 'get', 'buffer', 'close' or 'flush'
5769   * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5770   */
5771  function get_mailer($action='get') {
5772      global $CFG;
5773  
5774      /** @var moodle_phpmailer $mailer */
5775      static $mailer  = null;
5776      static $counter = 0;
5777  
5778      if (!isset($CFG->smtpmaxbulk)) {
5779          $CFG->smtpmaxbulk = 1;
5780      }
5781  
5782      if ($action == 'get') {
5783          $prevkeepalive = false;
5784  
5785          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5786              if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5787                  $counter++;
5788                  // Reset the mailer.
5789                  $mailer->Priority         = 3;
5790                  $mailer->CharSet          = 'UTF-8'; // Our default.
5791                  $mailer->ContentType      = "text/plain";
5792                  $mailer->Encoding         = "8bit";
5793                  $mailer->From             = "root@localhost";
5794                  $mailer->FromName         = "Root User";
5795                  $mailer->Sender           = "";
5796                  $mailer->Subject          = "";
5797                  $mailer->Body             = "";
5798                  $mailer->AltBody          = "";
5799                  $mailer->ConfirmReadingTo = "";
5800  
5801                  $mailer->clearAllRecipients();
5802                  $mailer->clearReplyTos();
5803                  $mailer->clearAttachments();
5804                  $mailer->clearCustomHeaders();
5805                  return $mailer;
5806              }
5807  
5808              $prevkeepalive = $mailer->SMTPKeepAlive;
5809              get_mailer('flush');
5810          }
5811  
5812          require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5813          $mailer = new moodle_phpmailer();
5814  
5815          $counter = 1;
5816  
5817          if ($CFG->smtphosts == 'qmail') {
5818              // Use Qmail system.
5819              $mailer->isQmail();
5820  
5821          } else if (empty($CFG->smtphosts)) {
5822              // Use PHP mail() = sendmail.
5823              $mailer->isMail();
5824  
5825          } else {
5826              // Use SMTP directly.
5827              $mailer->isSMTP();
5828              if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5829                  $mailer->SMTPDebug = 3;
5830              }
5831              // Specify main and backup servers.
5832              $mailer->Host          = $CFG->smtphosts;
5833              // Specify secure connection protocol.
5834              $mailer->SMTPSecure    = $CFG->smtpsecure;
5835              // Use previous keepalive.
5836              $mailer->SMTPKeepAlive = $prevkeepalive;
5837  
5838              if ($CFG->smtpuser) {
5839                  // Use SMTP authentication.
5840                  $mailer->SMTPAuth = true;
5841                  $mailer->Username = $CFG->smtpuser;
5842                  $mailer->Password = $CFG->smtppass;
5843              }
5844          }
5845  
5846          return $mailer;
5847      }
5848  
5849      $nothing = null;
5850  
5851      // Keep smtp session open after sending.
5852      if ($action == 'buffer') {
5853          if (!empty($CFG->smtpmaxbulk)) {
5854              get_mailer('flush');
5855              $m = get_mailer();
5856              if ($m->Mailer == 'smtp') {
5857                  $m->SMTPKeepAlive = true;
5858              }
5859          }
5860          return $nothing;
5861      }
5862  
5863      // Close smtp session, but continue buffering.
5864      if ($action == 'flush') {
5865          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5866              if (!empty($mailer->SMTPDebug)) {
5867                  echo '<pre>'."\n";
5868              }
5869              $mailer->SmtpClose();
5870              if (!empty($mailer->SMTPDebug)) {
5871                  echo '</pre>';
5872              }
5873          }
5874          return $nothing;
5875      }
5876  
5877      // Close smtp session, do not buffer anymore.
5878      if ($action == 'close') {
5879          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5880              get_mailer('flush');
5881              $mailer->SMTPKeepAlive = false;
5882          }
5883          $mailer = null; // Better force new instance.
5884          return $nothing;
5885      }
5886  }
5887  
5888  /**
5889   * A helper function to test for email diversion
5890   *
5891   * @param string $email
5892   * @return bool Returns true if the email should be diverted
5893   */
5894  function email_should_be_diverted($email) {
5895      global $CFG;
5896  
5897      if (empty($CFG->divertallemailsto)) {
5898          return false;
5899      }
5900  
5901      if (empty($CFG->divertallemailsexcept)) {
5902          return true;
5903      }
5904  
5905      $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5906      foreach ($patterns as $pattern) {
5907          if (preg_match("/$pattern/", $email)) {
5908              return false;
5909          }
5910      }
5911  
5912      return true;
5913  }
5914  
5915  /**
5916   * Generate a unique email Message-ID using the moodle domain and install path
5917   *
5918   * @param string $localpart An optional unique message id prefix.
5919   * @return string The formatted ID ready for appending to the email headers.
5920   */
5921  function generate_email_messageid($localpart = null) {
5922      global $CFG;
5923  
5924      $urlinfo = parse_url($CFG->wwwroot);
5925      $base = '@' . $urlinfo['host'];
5926  
5927      // If multiple moodles are on the same domain we want to tell them
5928      // apart so we add the install path to the local part. This means
5929      // that the id local part should never contain a / character so
5930      // we can correctly parse the id to reassemble the wwwroot.
5931      if (isset($urlinfo['path'])) {
5932          $base = $urlinfo['path'] . $base;
5933      }
5934  
5935      if (empty($localpart)) {
5936          $localpart = uniqid('', true);
5937      }
5938  
5939      // Because we may have an option /installpath suffix to the local part
5940      // of the id we need to escape any / chars which are in the $localpart.
5941      $localpart = str_replace('/', '%2F', $localpart);
5942  
5943      return '<' . $localpart . $base . '>';
5944  }
5945  
5946  /**
5947   * Send an email to a specified user
5948   *
5949   * @param stdClass $user  A {@link $USER} object
5950   * @param stdClass $from A {@link $USER} object
5951   * @param string $subject plain text subject line of the email
5952   * @param string $messagetext plain text version of the message
5953   * @param string $messagehtml complete html version of the message (optional)
5954   * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5955   *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5956   * @param string $attachname the name of the file (extension indicates MIME)
5957   * @param bool $usetrueaddress determines whether $from email address should
5958   *          be sent out. Will be overruled by user profile setting for maildisplay
5959   * @param string $replyto Email address to reply to
5960   * @param string $replytoname Name of reply to recipient
5961   * @param int $wordwrapwidth custom word wrap width, default 79
5962   * @return bool Returns true if mail was sent OK and false if there was an error.
5963   */
5964  function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5965                         $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5966  
5967      global $CFG, $PAGE, $SITE;
5968  
5969      if (empty($user) or empty($user->id)) {
5970          debugging('Can not send email to null user', DEBUG_DEVELOPER);
5971          return false;
5972      }
5973  
5974      if (empty($user->email)) {
5975          debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5976          return false;
5977      }
5978  
5979      if (!empty($user->deleted)) {
5980          debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5981          return false;
5982      }
5983  
5984      if (defined('BEHAT_SITE_RUNNING')) {
5985          // Fake email sending in behat.
5986          return true;
5987      }
5988  
5989      if (!empty($CFG->noemailever)) {
5990          // Hidden setting for development sites, set in config.php if needed.
5991          debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5992          return true;
5993      }
5994  
5995      if (email_should_be_diverted($user->email)) {
5996          $subject = "[DIVERTED {$user->email}] $subject";
5997          $user = clone($user);
5998          $user->email = $CFG->divertallemailsto;
5999      }
6000  
6001      // Skip mail to suspended users.
6002      if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
6003          return true;
6004      }
6005  
6006      if (!validate_email($user->email)) {
6007          // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6008          debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6009          return false;
6010      }
6011  
6012      if (over_bounce_threshold($user)) {
6013          debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6014          return false;
6015      }
6016  
6017      // TLD .invalid  is specifically reserved for invalid domain names.
6018      // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6019      if (substr($user->email, -8) == '.invalid') {
6020          debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6021          return true; // This is not an error.
6022      }
6023  
6024      // If the user is a remote mnet user, parse the email text for URL to the
6025      // wwwroot and modify the url to direct the user's browser to login at their
6026      // home site (identity provider - idp) before hitting the link itself.
6027      if (is_mnet_remote_user($user)) {
6028          require_once($CFG->dirroot.'/mnet/lib.php');
6029  
6030          $jumpurl = mnet_get_idp_jump_url($user);
6031          $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6032  
6033          $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6034                  $callback,
6035                  $messagetext);
6036          $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6037                  $callback,
6038                  $messagehtml);
6039      }
6040      $mail = get_mailer();
6041  
6042      if (!empty($mail->SMTPDebug)) {
6043          echo '<pre>' . "\n";
6044      }
6045  
6046      $temprecipients = array();
6047      $tempreplyto = array();
6048  
6049      // Make sure that we fall back onto some reasonable no-reply address.
6050      $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6051      $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6052  
6053      if (!validate_email($noreplyaddress)) {
6054          debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6055          $noreplyaddress = $noreplyaddressdefault;
6056      }
6057  
6058      // Make up an email address for handling bounces.
6059      if (!empty($CFG->handlebounces)) {
6060          $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6061          $mail->Sender = generate_email_processing_address(0, $modargs);
6062      } else {
6063          $mail->Sender = $noreplyaddress;
6064      }
6065  
6066      // Make sure that the explicit replyto is valid, fall back to the implicit one.
6067      if (!empty($replyto) && !validate_email($replyto)) {
6068          debugging('email_to_user: Invalid replyto-email '.s($replyto));
6069          $replyto = $noreplyaddress;
6070      }
6071  
6072      if (is_string($from)) { // So we can pass whatever we want if there is need.
6073          $mail->From     = $noreplyaddress;
6074          $mail->FromName = $from;
6075      // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6076      // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6077      // in a course with the sender.
6078      } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6079          if (!validate_email($from->email)) {
6080              debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6081              // Better not to use $noreplyaddress in this case.
6082              return false;
6083          }
6084          $mail->From = $from->email;
6085          $fromdetails = new stdClass();
6086          $fromdetails->name = fullname($from);
6087          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6088          $fromdetails->siteshortname = format_string($SITE->shortname);
6089          $fromstring = $fromdetails->name;
6090          if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6091              $fromstring = get_string('emailvia', 'core', $fromdetails);
6092          }
6093          $mail->FromName = $fromstring;
6094          if (empty($replyto)) {
6095              $tempreplyto[] = array($from->email, fullname($from));
6096          }
6097      } else {
6098          $mail->From = $noreplyaddress;
6099          $fromdetails = new stdClass();
6100          $fromdetails->name = fullname($from);
6101          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6102          $fromdetails->siteshortname = format_string($SITE->shortname);
6103          $fromstring = $fromdetails->name;
6104          if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6105              $fromstring = get_string('emailvia', 'core', $fromdetails);
6106          }
6107          $mail->FromName = $fromstring;
6108          if (empty($replyto)) {
6109              $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6110          }
6111      }
6112  
6113      if (!empty($replyto)) {
6114          $tempreplyto[] = array($replyto, $replytoname);
6115      }
6116  
6117      $temprecipients[] = array($user->email, fullname($user));
6118  
6119      // Set word wrap.
6120      $mail->WordWrap = $wordwrapwidth;
6121  
6122      if (!empty($from->customheaders)) {
6123          // Add custom headers.
6124          if (is_array($from->customheaders)) {
6125              foreach ($from->customheaders as $customheader) {
6126                  $mail->addCustomHeader($customheader);
6127              }
6128          } else {
6129              $mail->addCustomHeader($from->customheaders);
6130          }
6131      }
6132  
6133      // If the X-PHP-Originating-Script email header is on then also add an additional
6134      // header with details of where exactly in moodle the email was triggered from,
6135      // either a call to message_send() or to email_to_user().
6136      if (ini_get('mail.add_x_header')) {
6137  
6138          $stack = debug_backtrace(false);
6139          $origin = $stack[0];
6140  
6141          foreach ($stack as $depth => $call) {
6142              if ($call['function'] == 'message_send') {
6143                  $origin = $call;
6144              }
6145          }
6146  
6147          $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6148               . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6149          $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6150      }
6151  
6152      if (!empty($CFG->emailheaders)) {
6153          $headers = array_map('trim', explode("\n", $CFG->emailheaders));
6154          foreach ($headers as $header) {
6155              if (!empty($header)) {
6156                  $mail->addCustomHeader($header);
6157              }
6158          }
6159      }
6160  
6161      if (!empty($from->priority)) {
6162          $mail->Priority = $from->priority;
6163      }
6164  
6165      $renderer = $PAGE->get_renderer('core');
6166      $context = array(
6167          'sitefullname' => $SITE->fullname,
6168          'siteshortname' => $SITE->shortname,
6169          'sitewwwroot' => $CFG->wwwroot,
6170          'subject' => $subject,
6171          'prefix' => $CFG->emailsubjectprefix,
6172          'to' => $user->email,
6173          'toname' => fullname($user),
6174          'from' => $mail->From,
6175          'fromname' => $mail->FromName,
6176      );
6177      if (!empty($tempreplyto[0])) {
6178          $context['replyto'] = $tempreplyto[0][0];
6179          $context['replytoname'] = $tempreplyto[0][1];
6180      }
6181      if ($user->id > 0) {
6182          $context['touserid'] = $user->id;
6183          $context['tousername'] = $user->username;
6184      }
6185  
6186      if (!empty($user->mailformat) && $user->mailformat == 1) {
6187          // Only process html templates if the user preferences allow html email.
6188  
6189          if (!$messagehtml) {
6190              // If no html has been given, BUT there is an html wrapping template then
6191              // auto convert the text to html and then wrap it.
6192              $messagehtml = trim(text_to_html($messagetext));
6193          }
6194          $context['body'] = $messagehtml;
6195          $messagehtml = $renderer->render_from_template('core/email_html', $context);
6196      }
6197  
6198      $context['body'] = html_to_text(nl2br($messagetext));
6199      $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6200      $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6201      $messagetext = $renderer->render_from_template('core/email_text', $context);
6202  
6203      // Autogenerate a MessageID if it's missing.
6204      if (empty($mail->MessageID)) {
6205          $mail->MessageID = generate_email_messageid();
6206      }
6207  
6208      if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6209          // Don't ever send HTML to users who don't want it.
6210          $mail->isHTML(true);
6211          $mail->Encoding = 'quoted-printable';
6212          $mail->Body    =  $messagehtml;
6213          $mail->AltBody =  "\n$messagetext\n";
6214      } else {
6215          $mail->IsHTML(false);
6216          $mail->Body =  "\n$messagetext\n";
6217      }
6218  
6219      if ($attachment && $attachname) {
6220          if (preg_match( "~\\.\\.~" , $attachment )) {
6221              // Security check for ".." in dir path.
6222              $supportuser = core_user::get_support_user();
6223              $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6224              $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6225          } else {
6226              require_once($CFG->libdir.'/filelib.php');
6227              $mimetype = mimeinfo('type', $attachname);
6228  
6229              // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6230              // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6231              $attachpath = str_replace('\\', '/', realpath($attachment));
6232  
6233              // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6234              $allowedpaths = array_map(function(string $path): string {
6235                  return str_replace('\\', '/', realpath($path));
6236              }, [
6237                  $CFG->cachedir,
6238                  $CFG->dataroot,
6239                  $CFG->dirroot,
6240                  $CFG->localcachedir,
6241                  $CFG->tempdir,
6242                  $CFG->localrequestdir,
6243              ]);
6244  
6245              // Set addpath to true.
6246              $addpath = true;
6247  
6248              // Check if attachment includes one of the allowed paths.
6249              foreach (array_filter($allowedpaths) as $allowedpath) {
6250                  // Set addpath to false if the attachment includes one of the allowed paths.
6251                  if (strpos($attachpath, $allowedpath) === 0) {
6252                      $addpath = false;
6253                      break;
6254                  }
6255              }
6256  
6257              // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6258              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6259              if ($addpath == true) {
6260                  $attachment = $CFG->dataroot . '/' . $attachment;
6261              }
6262  
6263              $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6264          }
6265      }
6266  
6267      // Check if the email should be sent in an other charset then the default UTF-8.
6268      if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6269  
6270          // Use the defined site mail charset or eventually the one preferred by the recipient.
6271          $charset = $CFG->sitemailcharset;
6272          if (!empty($CFG->allowusermailcharset)) {
6273              if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6274                  $charset = $useremailcharset;
6275              }
6276          }
6277  
6278          // Convert all the necessary strings if the charset is supported.
6279          $charsets = get_list_of_charsets();
6280          unset($charsets['UTF-8']);
6281          if (in_array($charset, $charsets)) {
6282              $mail->CharSet  = $charset;
6283              $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6284              $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6285              $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6286              $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6287  
6288              foreach ($temprecipients as $key => $values) {
6289                  $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6290              }
6291              foreach ($tempreplyto as $key => $values) {
6292                  $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6293              }
6294          }
6295      }
6296  
6297      foreach ($temprecipients as $values) {
6298          $mail->addAddress($values[0], $values[1]);
6299      }
6300      foreach ($tempreplyto as $values) {
6301          $mail->addReplyTo($values[0], $values[1]);
6302      }
6303  
6304      if (!empty($CFG->emaildkimselector)) {
6305          $domain = substr(strrchr($mail->From, "@"), 1);
6306          $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6307          if (file_exists($pempath)) {
6308              $mail->DKIM_domain      = $domain;
6309              $mail->DKIM_private     = $pempath;
6310              $mail->DKIM_selector    = $CFG->emaildkimselector;
6311              $mail->DKIM_identity    = $mail->From;
6312          } else {
6313              debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6314          }
6315      }
6316  
6317      if ($mail->send()) {
6318          set_send_count($user);
6319          if (!empty($mail->SMTPDebug)) {
6320              echo '</pre>';
6321          }
6322          return true;
6323      } else {
6324          // Trigger event for failing to send email.
6325          $event = \core\event\email_failed::create(array(
6326              'context' => context_system::instance(),
6327              'userid' => $from->id,
6328              'relateduserid' => $user->id,
6329              'other' => array(
6330                  'subject' => $subject,
6331                  'message' => $messagetext,
6332                  'errorinfo' => $mail->ErrorInfo
6333              )
6334          ));
6335          $event->trigger();
6336          if (CLI_SCRIPT) {
6337              mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6338          }
6339          if (!empty($mail->SMTPDebug)) {
6340              echo '</pre>';
6341          }
6342          return false;
6343      }
6344  }
6345  
6346  /**
6347   * Check to see if a user's real email address should be used for the "From" field.
6348   *
6349   * @param  object $from The user object for the user we are sending the email from.
6350   * @param  object $user The user object that we are sending the email to.
6351   * @param  array $unused No longer used.
6352   * @return bool Returns true if we can use the from user's email adress in the "From" field.
6353   */
6354  function can_send_from_real_email_address($from, $user, $unused = null) {
6355      global $CFG;
6356      if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6357          return false;
6358      }
6359      $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6360      // Email is in the list of allowed domains for sending email,
6361      // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6362      // in a course with the sender.
6363      if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6364                  && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6365                  || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6366                  && enrol_get_shared_courses($user, $from, false, true)))) {
6367          return true;
6368      }
6369      return false;
6370  }
6371  
6372  /**
6373   * Generate a signoff for emails based on support settings
6374   *
6375   * @return string
6376   */
6377  function generate_email_signoff() {
6378      global $CFG;
6379  
6380      $signoff = "\n";
6381      if (!empty($CFG->supportname)) {
6382          $signoff .= $CFG->supportname."\n";
6383      }
6384      if (!empty($CFG->supportemail)) {
6385          $signoff .= $CFG->supportemail."\n";
6386      }
6387      if (!empty($CFG->supportpage)) {
6388          $signoff .= $CFG->supportpage."\n";
6389      }
6390      return $signoff;
6391  }
6392  
6393  /**
6394   * Sets specified user's password and send the new password to the user via email.
6395   *
6396   * @param stdClass $user A {@link $USER} object
6397   * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6398   * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6399   */
6400  function setnew_password_and_mail($user, $fasthash = false) {
6401      global $CFG, $DB;
6402  
6403      // We try to send the mail in language the user understands,
6404      // unfortunately the filter_string() does not support alternative langs yet
6405      // so multilang will not work properly for site->fullname.
6406      $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6407  
6408      $site  = get_site();
6409  
6410      $supportuser = core_user::get_support_user();
6411  
6412      $newpassword = generate_password();
6413  
6414      update_internal_user_password($user, $newpassword, $fasthash);
6415  
6416      $a = new stdClass();
6417      $a->firstname   = fullname($user, true);
6418      $a->sitename    = format_string($site->fullname);
6419      $a->username    = $user->username;
6420      $a->newpassword = $newpassword;
6421      $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6422      $a->signoff     = generate_email_signoff();
6423  
6424      $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6425  
6426      $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6427  
6428      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6429      return email_to_user($user, $supportuser, $subject, $message);
6430  
6431  }
6432  
6433  /**
6434   * Resets specified user's password and send the new password to the user via email.
6435   *
6436   * @param stdClass $user A {@link $USER} object
6437   * @return bool Returns true if mail was sent OK and false if there was an error.
6438   */
6439  function reset_password_and_mail($user) {
6440      global $CFG;
6441  
6442      $site  = get_site();
6443      $supportuser = core_user::get_support_user();
6444  
6445      $userauth = get_auth_plugin($user->auth);
6446      if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6447          trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6448          return false;
6449      }
6450  
6451      $newpassword = generate_password();
6452  
6453      if (!$userauth->user_update_password($user, $newpassword)) {
6454          print_error("cannotsetpassword");
6455      }
6456  
6457      $a = new stdClass();
6458      $a->firstname   = $user->firstname;
6459      $a->lastname    = $user->lastname;
6460      $a->sitename    = format_string($site->fullname);
6461      $a->username    = $user->username;
6462      $a->newpassword = $newpassword;
6463      $a->link        = $CFG->wwwroot .'/login/change_password.php';
6464      $a->signoff     = generate_email_signoff();
6465  
6466      $message = get_string('newpasswordtext', '', $a);
6467  
6468      $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6469  
6470      unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6471  
6472      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6473      return email_to_user($user, $supportuser, $subject, $message);
6474  }
6475  
6476  /**
6477   * Send email to specified user with confirmation text and activation link.
6478   *
6479   * @param stdClass $user A {@link $USER} object
6480   * @param string $confirmationurl user confirmation URL
6481   * @return bool Returns true if mail was sent OK and false if there was an error.
6482   */
6483  function send_confirmation_email($user, $confirmationurl = null) {
6484      global $CFG;
6485  
6486      $site = get_site();
6487      $supportuser = core_user::get_support_user();
6488  
6489      $data = new stdClass();
6490      $data->sitename  = format_string($site->fullname);
6491      $data->admin     = generate_email_signoff();
6492  
6493      $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6494  
6495      if (empty($confirmationurl)) {
6496          $confirmationurl = '/login/confirm.php';
6497      }
6498  
6499      $confirmationurl = new moodle_url($confirmationurl);
6500      // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6501      $confirmationurl->remove_params('data');
6502      $confirmationpath = $confirmationurl->out(false);
6503  
6504      // We need to custom encode the username to include trailing dots in the link.
6505      // Because of this custom encoding we can't use moodle_url directly.
6506      // Determine if a query string is present in the confirmation url.
6507      $hasquerystring = strpos($confirmationpath, '?') !== false;
6508      // Perform normal url encoding of the username first.
6509      $username = urlencode($user->username);
6510      // Prevent problems with trailing dots not being included as part of link in some mail clients.
6511      $username = str_replace('.', '%2E', $username);
6512  
6513      $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6514  
6515      $message     = get_string('emailconfirmation', '', $data);
6516      $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6517  
6518      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6519      return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6520  }
6521  
6522  /**
6523   * Sends a password change confirmation email.
6524   *
6525   * @param stdClass $user A {@link $USER} object
6526   * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6527   * @return bool Returns true if mail was sent OK and false if there was an error.
6528   */
6529  function send_password_change_confirmation_email($user, $resetrecord) {
6530      global $CFG;
6531  
6532      $site = get_site();
6533      $supportuser = core_user::get_support_user();
6534      $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6535  
6536      $data = new stdClass();
6537      $data->firstname = $user->firstname;
6538      $data->lastname  = $user->lastname;
6539      $data->username  = $user->username;
6540      $data->sitename  = format_string($site->fullname);
6541      $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6542      $data->admin     = generate_email_signoff();
6543      $data->resetminutes = $pwresetmins;
6544  
6545      $message = get_string('emailresetconfirmation', '', $data);
6546      $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6547  
6548      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6549      return email_to_user($user, $supportuser, $subject, $message);
6550  
6551  }
6552  
6553  /**
6554   * Sends an email containing information on how to change your password.
6555   *
6556   * @param stdClass $user A {@link $USER} object
6557   * @return bool Returns true if mail was sent OK and false if there was an error.
6558   */
6559  function send_password_change_info($user) {
6560      $site = get_site();
6561      $supportuser = core_user::get_support_user();
6562  
6563      $data = new stdClass();
6564      $data->firstname = $user->firstname;
6565      $data->lastname  = $user->lastname;
6566      $data->username  = $user->username;
6567      $data->sitename  = format_string($site->fullname);
6568      $data->admin     = generate_email_signoff();
6569  
6570      if (!is_enabled_auth($user->auth)) {
6571          $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6572          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6573          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6574          return email_to_user($user, $supportuser, $subject, $message);
6575      }
6576  
6577      $userauth = get_auth_plugin($user->auth);
6578      ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6579  
6580      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6581      return email_to_user($user, $supportuser, $subject, $message);
6582  }
6583  
6584  /**
6585   * Check that an email is allowed.  It returns an error message if there was a problem.
6586   *
6587   * @param string $email Content of email
6588   * @return string|false
6589   */
6590  function email_is_not_allowed($email) {
6591      global $CFG;
6592  
6593      // Comparing lowercase domains.
6594      $email = strtolower($email);
6595      if (!empty($CFG->allowemailaddresses)) {
6596          $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6597          foreach ($allowed as $allowedpattern) {
6598              $allowedpattern = trim($allowedpattern);
6599              if (!$allowedpattern) {
6600                  continue;
6601              }
6602              if (strpos($allowedpattern, '.') === 0) {
6603                  if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6604                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6605                      return false;
6606                  }
6607  
6608              } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6609                  return false;
6610              }
6611          }
6612          return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6613  
6614      } else if (!empty($CFG->denyemailaddresses)) {
6615          $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6616          foreach ($denied as $deniedpattern) {
6617              $deniedpattern = trim($deniedpattern);
6618              if (!$deniedpattern) {
6619                  continue;
6620              }
6621              if (strpos($deniedpattern, '.') === 0) {
6622                  if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6623                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6624                      return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6625                  }
6626  
6627              } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6628                  return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6629              }
6630          }
6631      }
6632  
6633      return false;
6634  }
6635  
6636  // FILE HANDLING.
6637  
6638  /**
6639   * Returns local file storage instance
6640   *
6641   * @return file_storage
6642   */
6643  function get_file_storage($reset = false) {
6644      global $CFG;
6645  
6646      static $fs = null;
6647  
6648      if ($reset) {
6649          $fs = null;
6650          return;
6651      }
6652  
6653      if ($fs) {
6654          return $fs;
6655      }
6656  
6657      require_once("$CFG->libdir/filelib.php");
6658  
6659      $fs = new file_storage();
6660  
6661      return $fs;
6662  }
6663  
6664  /**
6665   * Returns local file storage instance
6666   *
6667   * @return file_browser
6668   */
6669  function get_file_browser() {
6670      global $CFG;
6671  
6672      static $fb = null;
6673  
6674      if ($fb) {
6675          return $fb;
6676      }
6677  
6678      require_once("$CFG->libdir/filelib.php");
6679  
6680      $fb = new file_browser();
6681  
6682      return $fb;
6683  }
6684  
6685  /**
6686   * Returns file packer
6687   *
6688   * @param string $mimetype default application/zip
6689   * @return file_packer
6690   */
6691  function get_file_packer($mimetype='application/zip') {
6692      global $CFG;
6693  
6694      static $fp = array();
6695  
6696      if (isset($fp[$mimetype])) {
6697          return $fp[$mimetype];
6698      }
6699  
6700      switch ($mimetype) {
6701          case 'application/zip':
6702          case 'application/vnd.moodle.profiling':
6703              $classname = 'zip_packer';
6704              break;
6705  
6706          case 'application/x-gzip' :
6707              $classname = 'tgz_packer';
6708              break;
6709  
6710          case 'application/vnd.moodle.backup':
6711              $classname = 'mbz_packer';
6712              break;
6713  
6714          default:
6715              return false;
6716      }
6717  
6718      require_once("$CFG->libdir/filestorage/$classname.php");
6719      $fp[$mimetype] = new $classname();
6720  
6721      return $fp[$mimetype];
6722  }
6723  
6724  /**
6725   * Returns current name of file on disk if it exists.
6726   *
6727   * @param string $newfile File to be verified
6728   * @return string Current name of file on disk if true
6729   */
6730  function valid_uploaded_file($newfile) {
6731      if (empty($newfile)) {
6732          return '';
6733      }
6734      if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6735          return $newfile['tmp_name'];
6736      } else {
6737          return '';
6738      }
6739  }
6740  
6741  /**
6742   * Returns the maximum size for uploading files.
6743   *
6744   * There are seven possible upload limits:
6745   * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6746   * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6747   * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6748   * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6749   * 5. by the Moodle admin in $CFG->maxbytes
6750   * 6. by the teacher in the current course $course->maxbytes
6751   * 7. by the teacher for the current module, eg $assignment->maxbytes
6752   *
6753   * These last two are passed to this function as arguments (in bytes).
6754   * Anything defined as 0 is ignored.
6755   * The smallest of all the non-zero numbers is returned.
6756   *
6757   * @todo Finish documenting this function
6758   *
6759   * @param int $sitebytes Set maximum size
6760   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6761   * @param int $modulebytes Current module ->maxbytes (in bytes)
6762   * @param bool $unused This parameter has been deprecated and is not used any more.
6763   * @return int The maximum size for uploading files.
6764   */
6765  function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6766  
6767      if (! $filesize = ini_get('upload_max_filesize')) {
6768          $filesize = '5M';
6769      }
6770      $minimumsize = get_real_size($filesize);
6771  
6772      if ($postsize = ini_get('post_max_size')) {
6773          $postsize = get_real_size($postsize);
6774          if ($postsize < $minimumsize) {
6775              $minimumsize = $postsize;
6776          }
6777      }
6778  
6779      if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6780          $minimumsize = $sitebytes;
6781      }
6782  
6783      if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6784          $minimumsize = $coursebytes;
6785      }
6786  
6787      if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6788          $minimumsize = $modulebytes;
6789      }
6790  
6791      return $minimumsize;
6792  }
6793  
6794  /**
6795   * Returns the maximum size for uploading files for the current user
6796   *
6797   * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6798   *
6799   * @param context $context The context in which to check user capabilities
6800   * @param int $sitebytes Set maximum size
6801   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6802   * @param int $modulebytes Current module ->maxbytes (in bytes)
6803   * @param stdClass $user The user
6804   * @param bool $unused This parameter has been deprecated and is not used any more.
6805   * @return int The maximum size for uploading files.
6806   */
6807  function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6808          $unused = false) {
6809      global $USER;
6810  
6811      if (empty($user)) {
6812          $user = $USER;
6813      }
6814  
6815      if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6816          return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6817      }
6818  
6819      return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6820  }
6821  
6822  /**
6823   * Returns an array of possible sizes in local language
6824   *
6825   * Related to {@link get_max_upload_file_size()} - this function returns an
6826   * array of possible sizes in an array, translated to the
6827   * local language.
6828   *
6829   * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6830   *
6831   * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6832   * with the value set to 0. This option will be the first in the list.
6833   *
6834   * @uses SORT_NUMERIC
6835   * @param int $sitebytes Set maximum size
6836   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6837   * @param int $modulebytes Current module ->maxbytes (in bytes)
6838   * @param int|array $custombytes custom upload size/s which will be added to list,
6839   *        Only value/s smaller then maxsize will be added to list.
6840   * @return array
6841   */
6842  function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6843      global $CFG;
6844  
6845      if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6846          return array();
6847      }
6848  
6849      if ($sitebytes == 0) {
6850          // Will get the minimum of upload_max_filesize or post_max_size.
6851          $sitebytes = get_max_upload_file_size();
6852      }
6853  
6854      $filesize = array();
6855      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6856                        5242880, 10485760, 20971520, 52428800, 104857600,
6857                        262144000, 524288000, 786432000, 1073741824,
6858                        2147483648, 4294967296, 8589934592);
6859  
6860      // If custombytes is given and is valid then add it to the list.
6861      if (is_number($custombytes) and $custombytes > 0) {
6862          $custombytes = (int)$custombytes;
6863          if (!in_array($custombytes, $sizelist)) {
6864              $sizelist[] = $custombytes;
6865          }
6866      } else if (is_array($custombytes)) {
6867          $sizelist = array_unique(array_merge($sizelist, $custombytes));
6868      }
6869  
6870      // Allow maxbytes to be selected if it falls outside the above boundaries.
6871      if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6872          // Note: get_real_size() is used in order to prevent problems with invalid values.
6873          $sizelist[] = get_real_size($CFG->maxbytes);
6874      }
6875  
6876      foreach ($sizelist as $sizebytes) {
6877          if ($sizebytes < $maxsize && $sizebytes > 0) {
6878              $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6879          }
6880      }
6881  
6882      $limitlevel = '';
6883      $displaysize = '';
6884      if ($modulebytes &&
6885          (($modulebytes < $coursebytes || $coursebytes == 0) &&
6886           ($modulebytes < $sitebytes || $sitebytes == 0))) {
6887          $limitlevel = get_string('activity', 'core');
6888          $displaysize = display_size($modulebytes, 0);
6889          $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6890  
6891      } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6892          $limitlevel = get_string('course', 'core');
6893          $displaysize = display_size($coursebytes, 0);
6894          $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6895  
6896      } else if ($sitebytes) {
6897          $limitlevel = get_string('site', 'core');
6898          $displaysize = display_size($sitebytes, 0);
6899          $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6900      }
6901  
6902      krsort($filesize, SORT_NUMERIC);
6903      if ($limitlevel) {
6904          $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6905          $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6906      }
6907  
6908      return $filesize;
6909  }
6910  
6911  /**
6912   * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6913   *
6914   * If excludefiles is defined, then that file/directory is ignored
6915   * If getdirs is true, then (sub)directories are included in the output
6916   * If getfiles is true, then files are included in the output
6917   * (at least one of these must be true!)
6918   *
6919   * @todo Finish documenting this function. Add examples of $excludefile usage.
6920   *
6921   * @param string $rootdir A given root directory to start from
6922   * @param string|array $excludefiles If defined then the specified file/directory is ignored
6923   * @param bool $descend If true then subdirectories are recursed as well
6924   * @param bool $getdirs If true then (sub)directories are included in the output
6925   * @param bool $getfiles  If true then files are included in the output
6926   * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6927   */
6928  function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6929  
6930      $dirs = array();
6931  
6932      if (!$getdirs and !$getfiles) {   // Nothing to show.
6933          return $dirs;
6934      }
6935  
6936      if (!is_dir($rootdir)) {          // Must be a directory.
6937          return $dirs;
6938      }
6939  
6940      if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6941          return $dirs;
6942      }
6943  
6944      if (!is_array($excludefiles)) {
6945          $excludefiles = array($excludefiles);
6946      }
6947  
6948      while (false !== ($file = readdir($dir))) {
6949          $firstchar = substr($file, 0, 1);
6950          if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6951              continue;
6952          }
6953          $fullfile = $rootdir .'/'. $file;
6954          if (filetype($fullfile) == 'dir') {
6955              if ($getdirs) {
6956                  $dirs[] = $file;
6957              }
6958              if ($descend) {
6959                  $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6960                  foreach ($subdirs as $subdir) {
6961                      $dirs[] = $file .'/'. $subdir;
6962                  }
6963              }
6964          } else if ($getfiles) {
6965              $dirs[] = $file;
6966          }
6967      }
6968      closedir($dir);
6969  
6970      asort($dirs);
6971  
6972      return $dirs;
6973  }
6974  
6975  
6976  /**
6977   * Adds up all the files in a directory and works out the size.
6978   *
6979   * @param string $rootdir  The directory to start from
6980   * @param string $excludefile A file to exclude when summing directory size
6981   * @return int The summed size of all files and subfiles within the root directory
6982   */
6983  function get_directory_size($rootdir, $excludefile='') {
6984      global $CFG;
6985  
6986      // Do it this way if we can, it's much faster.
6987      if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6988          $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6989          $output = null;
6990          $return = null;
6991          exec($command, $output, $return);
6992          if (is_array($output)) {
6993              // We told it to return k.
6994              return get_real_size(intval($output[0]).'k');
6995          }
6996      }
6997  
6998      if (!is_dir($rootdir)) {
6999          // Must be a directory.
7000          return 0;
7001      }
7002  
7003      if (!$dir = @opendir($rootdir)) {
7004          // Can't open it for some reason.
7005          return 0;
7006      }
7007  
7008      $size = 0;
7009  
7010      while (false !== ($file = readdir($dir))) {
7011          $firstchar = substr($file, 0, 1);
7012          if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7013              continue;
7014          }
7015          $fullfile = $rootdir .'/'. $file;
7016          if (filetype($fullfile) == 'dir') {
7017              $size += get_directory_size($fullfile, $excludefile);
7018          } else {
7019              $size += filesize($fullfile);
7020          }
7021      }
7022      closedir($dir);
7023  
7024      return $size;
7025  }
7026  
7027  /**
7028   * Converts bytes into display form
7029   *
7030   * @param int $size  The size to convert to human readable form
7031   * @param int $decimalplaces If specified, uses fixed number of decimal places
7032   * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
7033   * @return string Display version of size
7034   */
7035  function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
7036  
7037      static $units;
7038  
7039      if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
7040          return get_string('unlimited');
7041      }
7042  
7043      if (empty($units)) {
7044          $units[] = get_string('sizeb');
7045          $units[] = get_string('sizekb');
7046          $units[] = get_string('sizemb');
7047          $units[] = get_string('sizegb');
7048          $units[] = get_string('sizetb');
7049          $units[] = get_string('sizepb');
7050      }
7051  
7052      switch ($fixedunits) {
7053          case 'PB' :
7054              $magnitude = 5;
7055              break;
7056          case 'TB' :
7057              $magnitude = 4;
7058              break;
7059          case 'GB' :
7060              $magnitude = 3;
7061              break;
7062          case 'MB' :
7063              $magnitude = 2;
7064              break;
7065          case 'KB' :
7066              $magnitude = 1;
7067              break;
7068          case 'B' :
7069              $magnitude = 0;
7070              break;
7071          case '':
7072              $magnitude = floor(log($size, 1024));
7073              $magnitude = max(0, min(5, $magnitude));
7074              break;
7075          default:
7076              throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
7077      }
7078  
7079      // Special case for magnitude 0 (bytes) - never use decimal places.
7080      $nbsp = "\xc2\xa0";
7081      if ($magnitude === 0) {
7082          return round($size) . $nbsp . $units[$magnitude];
7083      }
7084  
7085      // Convert to specified units.
7086      $sizeinunit = $size / 1024 ** $magnitude;
7087  
7088      // Fixed decimal places.
7089      return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
7090  }
7091  
7092  /**
7093   * Cleans a given filename by removing suspicious or troublesome characters
7094   *
7095   * @see clean_param()
7096   * @param string $string file name
7097   * @return string cleaned file name
7098   */
7099  function clean_filename($string) {
7100      return clean_param($string, PARAM_FILE);
7101  }
7102  
7103  // STRING TRANSLATION.
7104  
7105  /**
7106   * Returns the code for the current language
7107   *
7108   * @category string
7109   * @return string
7110   */
7111  function current_language() {
7112      global $CFG, $USER, $SESSION, $COURSE;
7113  
7114      if (!empty($SESSION->forcelang)) {
7115          // Allows overriding course-forced language (useful for admins to check
7116          // issues in courses whose language they don't understand).
7117          // Also used by some code to temporarily get language-related information in a
7118          // specific language (see force_current_language()).
7119          $return = $SESSION->forcelang;
7120  
7121      } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
7122          // Course language can override all other settings for this page.
7123          $return = $COURSE->lang;
7124  
7125      } else if (!empty($SESSION->lang)) {
7126          // Session language can override other settings.
7127          $return = $SESSION->lang;
7128  
7129      } else if (!empty($USER->lang)) {
7130          $return = $USER->lang;
7131  
7132      } else if (isset($CFG->lang)) {
7133          $return = $CFG->lang;
7134  
7135      } else {
7136          $return = 'en';
7137      }
7138  
7139      // Just in case this slipped in from somewhere by accident.
7140      $return = str_replace('_utf8', '', $return);
7141  
7142      return $return;
7143  }
7144  
7145  /**
7146   * Fix the current language to the given language code.
7147   *
7148   * @param string $lang The language code to use.
7149   * @return void
7150   */
7151  function fix_current_language(string $lang): void {
7152      global $CFG, $COURSE, $SESSION, $USER;
7153  
7154      if (!get_string_manager()->translation_exists($lang)) {
7155          throw new coding_exception("The language pack for $lang is not available");
7156      }
7157  
7158      $fixglobal = '';
7159      $fixlang = 'lang';
7160      if (!empty($SESSION->forcelang)) {
7161          $fixglobal = $SESSION;
7162          $fixlang = 'forcelang';
7163      } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
7164          $fixglobal = $COURSE;
7165      } else if (!empty($SESSION->lang)) {
7166          $fixglobal = $SESSION;
7167      } else if (!empty($USER->lang)) {
7168          $fixglobal = $USER;
7169      } else if (isset($CFG->lang)) {
7170          set_config('lang', $lang);
7171      }
7172  
7173      if ($fixglobal) {
7174          $fixglobal->$fixlang = $lang;
7175      }
7176  }
7177  
7178  /**
7179   * Returns parent language of current active language if defined
7180   *
7181   * @category string
7182   * @param string $lang null means current language
7183   * @return string
7184   */
7185  function get_parent_language($lang=null) {
7186  
7187      $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7188  
7189      if ($parentlang === 'en') {
7190          $parentlang = '';
7191      }
7192  
7193      return $parentlang;
7194  }
7195  
7196  /**
7197   * Force the current language to get strings and dates localised in the given language.
7198   *
7199   * After calling this function, all strings will be provided in the given language
7200   * until this function is called again, or equivalent code is run.
7201   *
7202   * @param string $language
7203   * @return string previous $SESSION->forcelang value
7204   */
7205  function force_current_language($language) {
7206      global $SESSION;
7207      $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7208      if ($language !== $sessionforcelang) {
7209          // Seting forcelang to null or an empty string disables it's effect.
7210          if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7211              $SESSION->forcelang = $language;
7212              moodle_setlocale();
7213          }
7214      }
7215      return $sessionforcelang;
7216  }
7217  
7218  /**
7219   * Returns current string_manager instance.
7220   *
7221   * The param $forcereload is needed for CLI installer only where the string_manager instance
7222   * must be replaced during the install.php script life time.
7223   *
7224   * @category string
7225   * @param bool $forcereload shall the singleton be released and new instance created instead?
7226   * @return core_string_manager
7227   */
7228  function get_string_manager($forcereload=false) {
7229      global $CFG;
7230  
7231      static $singleton = null;
7232  
7233      if ($forcereload) {
7234          $singleton = null;
7235      }
7236      if ($singleton === null) {
7237          if (empty($CFG->early_install_lang)) {
7238  
7239              $transaliases = array();
7240              if (empty($CFG->langlist)) {
7241                   $translist = array();
7242              } else {
7243                  $translist = explode(',', $CFG->langlist);
7244                  $translist = array_map('trim', $translist);
7245                  // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7246                  foreach ($translist as $i => $value) {
7247                      $parts = preg_split('/\s*\|\s*/', $value, 2);
7248                      if (count($parts) == 2) {
7249                          $transaliases[$parts[0]] = $parts[1];
7250                          $translist[$i] = $parts[0];
7251                      }
7252                  }
7253              }
7254  
7255              if (!empty($CFG->config_php_settings['customstringmanager'])) {
7256                  $classname = $CFG->config_php_settings['customstringmanager'];
7257  
7258                  if (class_exists($classname)) {
7259                      $implements = class_implements($classname);
7260  
7261                      if (isset($implements['core_string_manager'])) {
7262                          $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7263                          return $singleton;
7264  
7265                      } else {
7266                          debugging('Unable to instantiate custom string manager: class '.$classname.
7267                              ' does not implement the core_string_manager interface.');
7268                      }
7269  
7270                  } else {
7271                      debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7272                  }
7273              }
7274  
7275              $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7276  
7277          } else {
7278              $singleton = new core_string_manager_install();
7279          }
7280      }
7281  
7282      return $singleton;
7283  }
7284  
7285  /**
7286   * Returns a localized string.
7287   *
7288   * Returns the translated string specified by $identifier as
7289   * for $module.  Uses the same format files as STphp.
7290   * $a is an object, string or number that can be used
7291   * within translation strings
7292   *
7293   * eg 'hello {$a->firstname} {$a->lastname}'
7294   * or 'hello {$a}'
7295   *
7296   * If you would like to directly echo the localized string use
7297   * the function {@link print_string()}
7298   *
7299   * Example usage of this function involves finding the string you would
7300   * like a local equivalent of and using its identifier and module information
7301   * to retrieve it.<br/>
7302   * If you open moodle/lang/en/moodle.php and look near line 278
7303   * you will find a string to prompt a user for their word for 'course'
7304   * <code>
7305   * $string['course'] = 'Course';
7306   * </code>
7307   * So if you want to display the string 'Course'
7308   * in any language that supports it on your site
7309   * you just need to use the identifier 'course'
7310   * <code>
7311   * $mystring = '<strong>'. get_string('course') .'</strong>';
7312   * or
7313   * </code>
7314   * If the string you want is in another file you'd take a slightly
7315   * different approach. Looking in moodle/lang/en/calendar.php you find
7316   * around line 75:
7317   * <code>
7318   * $string['typecourse'] = 'Course event';
7319   * </code>
7320   * If you want to display the string "Course event" in any language
7321   * supported you would use the identifier 'typecourse' and the module 'calendar'
7322   * (because it is in the file calendar.php):
7323   * <code>
7324   * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7325   * </code>
7326   *
7327   * As a last resort, should the identifier fail to map to a string
7328   * the returned string will be [[ $identifier ]]
7329   *
7330   * In Moodle 2.3 there is a new argument to this function $lazyload.
7331   * Setting $lazyload to true causes get_string to return a lang_string object
7332   * rather than the string itself. The fetching of the string is then put off until
7333   * the string object is first used. The object can be used by calling it's out
7334   * method or by casting the object to a string, either directly e.g.
7335   *     (string)$stringobject
7336   * or indirectly by using the string within another string or echoing it out e.g.
7337   *     echo $stringobject
7338   *     return "<p>{$stringobject}</p>";
7339   * It is worth noting that using $lazyload and attempting to use the string as an
7340   * array key will cause a fatal error as objects cannot be used as array keys.
7341   * But you should never do that anyway!
7342   * For more information {@link lang_string}
7343   *
7344   * @category string
7345   * @param string $identifier The key identifier for the localized string
7346   * @param string $component The module where the key identifier is stored,
7347   *      usually expressed as the filename in the language pack without the
7348   *      .php on the end but can also be written as mod/forum or grade/export/xls.
7349   *      If none is specified then moodle.php is used.
7350   * @param string|object|array $a An object, string or number that can be used
7351   *      within translation strings
7352   * @param bool $lazyload If set to true a string object is returned instead of
7353   *      the string itself. The string then isn't calculated until it is first used.
7354   * @return string The localized string.
7355   * @throws coding_exception
7356   */
7357  function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7358      global $CFG;
7359  
7360      // If the lazy load argument has been supplied return a lang_string object
7361      // instead.
7362      // We need to make sure it is true (and a bool) as you will see below there
7363      // used to be a forth argument at one point.
7364      if ($lazyload === true) {
7365          return new lang_string($identifier, $component, $a);
7366      }
7367  
7368      if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7369          throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7370      }
7371  
7372      // There is now a forth argument again, this time it is a boolean however so
7373      // we can still check for the old extralocations parameter.
7374      if (!is_bool($lazyload) && !empty($lazyload)) {
7375          debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7376      }
7377  
7378      if (strpos($component, '/') !== false) {
7379          debugging('The module name you passed to get_string is the deprecated format ' .
7380                  'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7381          $componentpath = explode('/', $component);
7382  
7383          switch ($componentpath[0]) {
7384              case 'mod':
7385                  $component = $componentpath[1];
7386                  break;
7387              case 'blocks':
7388              case 'block':
7389                  $component = 'block_'.$componentpath[1];
7390                  break;
7391              case 'enrol':
7392                  $component = 'enrol_'.$componentpath[1];
7393                  break;
7394              case 'format':
7395                  $component = 'format_'.$componentpath[1];
7396                  break;
7397              case 'grade':
7398                  $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7399                  break;
7400          }
7401      }
7402  
7403      $result = get_string_manager()->get_string($identifier, $component, $a);
7404  
7405      // Debugging feature lets you display string identifier and component.
7406      if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7407          $result .= ' {' . $identifier . '/' . $component . '}';
7408      }
7409      return $result;
7410  }
7411  
7412  /**
7413   * Converts an array of strings to their localized value.
7414   *
7415   * @param array $array An array of strings
7416   * @param string $component The language module that these strings can be found in.
7417   * @return stdClass translated strings.
7418   */
7419  function get_strings($array, $component = '') {
7420      $string = new stdClass;
7421      foreach ($array as $item) {
7422          $string->$item = get_string($item, $component);
7423      }
7424      return $string;
7425  }
7426  
7427  /**
7428   * Prints out a translated string.
7429   *
7430   * Prints out a translated string using the return value from the {@link get_string()} function.
7431   *
7432   * Example usage of this function when the string is in the moodle.php file:<br/>
7433   * <code>
7434   * echo '<strong>';
7435   * print_string('course');
7436   * echo '</strong>';
7437   * </code>
7438   *
7439   * Example usage of this function when the string is not in the moodle.php file:<br/>
7440   * <code>
7441   * echo '<h1>';
7442   * print_string('typecourse', 'calendar');
7443   * echo '</h1>';
7444   * </code>
7445   *
7446   * @category string
7447   * @param string $identifier The key identifier for the localized string
7448   * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7449   * @param string|object|array $a An object, string or number that can be used within translation strings
7450   */
7451  function print_string($identifier, $component = '', $a = null) {
7452      echo get_string($identifier, $component, $a);
7453  }
7454  
7455  /**
7456   * Returns a list of charset codes
7457   *
7458   * Returns a list of charset codes. It's hardcoded, so they should be added manually
7459   * (checking that such charset is supported by the texlib library!)
7460   *
7461   * @return array And associative array with contents in the form of charset => charset
7462   */
7463  function get_list_of_charsets() {
7464  
7465      $charsets = array(
7466          'EUC-JP'     => 'EUC-JP',
7467          'ISO-2022-JP'=> 'ISO-2022-JP',
7468          'ISO-8859-1' => 'ISO-8859-1',
7469          'SHIFT-JIS'  => 'SHIFT-JIS',
7470          'GB2312'     => 'GB2312',
7471          'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7472          'UTF-8'      => 'UTF-8');
7473  
7474      asort($charsets);
7475  
7476      return $charsets;
7477  }
7478  
7479  /**
7480   * Returns a list of valid and compatible themes
7481   *
7482   * @return array
7483   */
7484  function get_list_of_themes() {
7485      global $CFG;
7486  
7487      $themes = array();
7488  
7489      if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7490          $themelist = explode(',', $CFG->themelist);
7491      } else {
7492          $themelist = array_keys(core_component::get_plugin_list("theme"));
7493      }
7494  
7495      foreach ($themelist as $key => $themename) {
7496          $theme = theme_config::load($themename);
7497          $themes[$themename] = $theme;
7498      }
7499  
7500      core_collator::asort_objects_by_method($themes, 'get_theme_name');
7501  
7502      return $themes;
7503  }
7504  
7505  /**
7506   * Factory function for emoticon_manager
7507   *
7508   * @return emoticon_manager singleton
7509   */
7510  function get_emoticon_manager() {
7511      static $singleton = null;
7512  
7513      if (is_null($singleton)) {
7514          $singleton = new emoticon_manager();
7515      }
7516  
7517      return $singleton;
7518  }
7519  
7520  /**
7521   * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7522   *
7523   * Whenever this manager mentiones 'emoticon object', the following data
7524   * structure is expected: stdClass with properties text, imagename, imagecomponent,
7525   * altidentifier and altcomponent
7526   *
7527   * @see admin_setting_emoticons
7528   *
7529   * @copyright 2010 David Mudrak
7530   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7531   */
7532  class emoticon_manager {
7533  
7534      /**
7535       * Returns the currently enabled emoticons
7536       *
7537       * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7538       * @return array of emoticon objects
7539       */
7540      public function get_emoticons($selectable = false) {
7541          global $CFG;
7542          $notselectable = ['martin', 'egg'];
7543  
7544          if (empty($CFG->emoticons)) {
7545              return array();
7546          }
7547  
7548          $emoticons = $this->decode_stored_config($CFG->emoticons);
7549  
7550          if (!is_array($emoticons)) {
7551              // Something is wrong with the format of stored setting.
7552              debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7553              return array();
7554          }
7555          if ($selectable) {
7556              foreach ($emoticons as $index => $emote) {
7557                  if (in_array($emote->altidentifier, $notselectable)) {
7558                      // Skip this one.
7559                      unset($emoticons[$index]);
7560                  }
7561              }
7562          }
7563  
7564          return $emoticons;
7565      }
7566  
7567      /**
7568       * Converts emoticon object into renderable pix_emoticon object
7569       *
7570       * @param stdClass $emoticon emoticon object
7571       * @param array $attributes explicit HTML attributes to set
7572       * @return pix_emoticon
7573       */
7574      public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7575          $stringmanager = get_string_manager();
7576          if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7577              $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7578          } else {
7579              $alt = s($emoticon->text);
7580          }
7581          return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7582      }
7583  
7584      /**
7585       * Encodes the array of emoticon objects into a string storable in config table
7586       *
7587       * @see self::decode_stored_config()
7588       * @param array $emoticons array of emtocion objects
7589       * @return string
7590       */
7591      public function encode_stored_config(array $emoticons) {
7592          return json_encode($emoticons);
7593      }
7594  
7595      /**
7596       * Decodes the string into an array of emoticon objects
7597       *
7598       * @see self::encode_stored_config()
7599       * @param string $encoded
7600       * @return string|null
7601       */
7602      public function decode_stored_config($encoded) {
7603          $decoded = json_decode($encoded);
7604          if (!is_array($decoded)) {
7605              return null;
7606          }
7607          return $decoded;
7608      }
7609  
7610      /**
7611       * Returns default set of emoticons supported by Moodle
7612       *
7613       * @return array of sdtClasses
7614       */
7615      public function default_emoticons() {
7616          return array(
7617              $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7618              $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7619              $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7620              $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7621              $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7622              $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7623              $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7624              $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7625              $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7626              $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7627              $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7628              $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7629              $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7630              $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7631              $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7632              $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7633              $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7634              $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7635              $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7636              $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7637              $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7638              $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7639              $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7640              $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7641              $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7642              $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7643              $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7644              $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7645              $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7646              $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7647          );
7648      }
7649  
7650      /**
7651       * Helper method preparing the stdClass with the emoticon properties
7652       *
7653       * @param string|array $text or array of strings
7654       * @param string $imagename to be used by {@link pix_emoticon}
7655       * @param string $altidentifier alternative string identifier, null for no alt
7656       * @param string $altcomponent where the alternative string is defined
7657       * @param string $imagecomponent to be used by {@link pix_emoticon}
7658       * @return stdClass
7659       */
7660      protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7661                                                 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7662          return (object)array(
7663              'text'           => $text,
7664              'imagename'      => $imagename,
7665              'imagecomponent' => $imagecomponent,
7666              'altidentifier'  => $altidentifier,
7667              'altcomponent'   => $altcomponent,
7668          );
7669      }
7670  }
7671  
7672  // ENCRYPTION.
7673  
7674  /**
7675   * rc4encrypt
7676   *
7677   * @param string $data        Data to encrypt.
7678   * @return string             The now encrypted data.
7679   */
7680  function rc4encrypt($data) {
7681      return endecrypt(get_site_identifier(), $data, '');
7682  }
7683  
7684  /**
7685   * rc4decrypt
7686   *
7687   * @param string $data        Data to decrypt.
7688   * @return string             The now decrypted data.
7689   */
7690  function rc4decrypt($data) {
7691      return endecrypt(get_site_identifier(), $data, 'de');
7692  }
7693  
7694  /**
7695   * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7696   *
7697   * @todo Finish documenting this function
7698   *
7699   * @param string $pwd The password to use when encrypting or decrypting
7700   * @param string $data The data to be decrypted/encrypted
7701   * @param string $case Either 'de' for decrypt or '' for encrypt
7702   * @return string
7703   */
7704  function endecrypt ($pwd, $data, $case) {
7705  
7706      if ($case == 'de') {
7707          $data = urldecode($data);
7708      }
7709  
7710      $key[] = '';
7711      $box[] = '';
7712      $pwdlength = strlen($pwd);
7713  
7714      for ($i = 0; $i <= 255; $i++) {
7715          $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7716          $box[$i] = $i;
7717      }
7718  
7719      $x = 0;
7720  
7721      for ($i = 0; $i <= 255; $i++) {
7722          $x = ($x + $box[$i] + $key[$i]) % 256;
7723          $tempswap = $box[$i];
7724          $box[$i] = $box[$x];
7725          $box[$x] = $tempswap;
7726      }
7727  
7728      $cipher = '';
7729  
7730      $a = 0;
7731      $j = 0;
7732  
7733      for ($i = 0; $i < strlen($data); $i++) {
7734          $a = ($a + 1) % 256;
7735          $j = ($j + $box[$a]) % 256;
7736          $temp = $box[$a];
7737          $box[$a] = $box[$j];
7738          $box[$j] = $temp;
7739          $k = $box[(($box[$a] + $box[$j]) % 256)];
7740          $cipherby = ord(substr($data, $i, 1)) ^ $k;
7741          $cipher .= chr($cipherby);
7742      }
7743  
7744      if ($case == 'de') {
7745          $cipher = urldecode(urlencode($cipher));
7746      } else {
7747          $cipher = urlencode($cipher);
7748      }
7749  
7750      return $cipher;
7751  }
7752  
7753  // ENVIRONMENT CHECKING.
7754  
7755  /**
7756   * This method validates a plug name. It is much faster than calling clean_param.
7757   *
7758   * @param string $name a string that might be a plugin name.
7759   * @return bool if this string is a valid plugin name.
7760   */
7761  function is_valid_plugin_name($name) {
7762      // This does not work for 'mod', bad luck, use any other type.
7763      return core_component::is_valid_plugin_name('tool', $name);
7764  }
7765  
7766  /**
7767   * Get a list of all the plugins of a given type that define a certain API function
7768   * in a certain file. The plugin component names and function names are returned.
7769   *
7770   * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7771   * @param string $function the part of the name of the function after the
7772   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7773   *      names like report_courselist_hook.
7774   * @param string $file the name of file within the plugin that defines the
7775   *      function. Defaults to lib.php.
7776   * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7777   *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7778   */
7779  function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7780      global $CFG;
7781  
7782      // We don't include here as all plugin types files would be included.
7783      $plugins = get_plugins_with_function($function, $file, false);
7784  
7785      if (empty($plugins[$plugintype])) {
7786          return array();
7787      }
7788  
7789      $allplugins = core_component::get_plugin_list($plugintype);
7790  
7791      // Reformat the array and include the files.
7792      $pluginfunctions = array();
7793      foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7794  
7795          // Check that it has not been removed and the file is still available.
7796          if (!empty($allplugins[$pluginname])) {
7797  
7798              $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7799              if (file_exists($filepath)) {
7800                  include_once($filepath);
7801  
7802                  // Now that the file is loaded, we must verify the function still exists.
7803                  if (function_exists($functionname)) {
7804                      $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7805                  } else {
7806                      // Invalidate the cache for next run.
7807                      \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7808                  }
7809              }
7810          }
7811      }
7812  
7813      return $pluginfunctions;
7814  }
7815  
7816  /**
7817   * Get a list of all the plugins that define a certain API function in a certain file.
7818   *
7819   * @param string $function the part of the name of the function after the
7820   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7821   *      names like report_courselist_hook.
7822   * @param string $file the name of file within the plugin that defines the
7823   *      function. Defaults to lib.php.
7824   * @param bool $include Whether to include the files that contain the functions or not.
7825   * @return array with [plugintype][plugin] = functionname
7826   */
7827  function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
7828      global $CFG;
7829  
7830      if (during_initial_install() || isset($CFG->upgraderunning)) {
7831          // API functions _must not_ be called during an installation or upgrade.
7832          return [];
7833      }
7834  
7835      $cache = \cache::make('core', 'plugin_functions');
7836  
7837      // Including both although I doubt that we will find two functions definitions with the same name.
7838      // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7839      $key = $function . '_' . clean_param($file, PARAM_ALPHA);
7840      $pluginfunctions = $cache->get($key);
7841      $dirty = false;
7842  
7843      // Use the plugin manager to check that plugins are currently installed.
7844      $pluginmanager = \core_plugin_manager::instance();
7845  
7846      if ($pluginfunctions !== false) {
7847  
7848          // Checking that the files are still available.
7849          foreach ($pluginfunctions as $plugintype => $plugins) {
7850  
7851              $allplugins = \core_component::get_plugin_list($plugintype);
7852              $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7853              foreach ($plugins as $plugin => $function) {
7854                  if (!isset($installedplugins[$plugin])) {
7855                      // Plugin code is still present on disk but it is not installed.
7856                      $dirty = true;
7857                      break 2;
7858                  }
7859  
7860                  // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7861                  if (empty($allplugins[$plugin])) {
7862                      $dirty = true;
7863                      break 2;
7864                  }
7865  
7866                  $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7867                  if ($include && $fileexists) {
7868                      // Include the files if it was requested.
7869                      include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7870                  } else if (!$fileexists) {
7871                      // If the file is not available any more it should not be returned.
7872                      $dirty = true;
7873                      break 2;
7874                  }
7875  
7876                  // Check if the function still exists in the file.
7877                  if ($include && !function_exists($function)) {
7878                      $dirty = true;
7879                      break 2;
7880                  }
7881              }
7882          }
7883  
7884          // If the cache is dirty, we should fall through and let it rebuild.
7885          if (!$dirty) {
7886              return $pluginfunctions;
7887          }
7888      }
7889  
7890      $pluginfunctions = array();
7891  
7892      // To fill the cached. Also, everything should continue working with cache disabled.
7893      $plugintypes = \core_component::get_plugin_types();
7894      foreach ($plugintypes as $plugintype => $unused) {
7895  
7896          // We need to include files here.
7897          $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7898          $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7899          foreach ($pluginswithfile as $plugin => $notused) {
7900  
7901              if (!isset($installedplugins[$plugin])) {
7902                  continue;
7903              }
7904  
7905              $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7906  
7907              $pluginfunction = false;
7908              if (function_exists($fullfunction)) {
7909                  // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7910                  $pluginfunction = $fullfunction;
7911  
7912              } else if ($plugintype === 'mod') {
7913                  // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7914                  $shortfunction = $plugin . '_' . $function;
7915                  if (function_exists($shortfunction)) {
7916                      $pluginfunction = $shortfunction;
7917                  }
7918              }
7919  
7920              if ($pluginfunction) {
7921                  if (empty($pluginfunctions[$plugintype])) {
7922                      $pluginfunctions[$plugintype] = array();
7923                  }
7924                  $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7925              }
7926  
7927          }
7928      }
7929      $cache->set($key, $pluginfunctions);
7930  
7931      return $pluginfunctions;
7932  
7933  }
7934  
7935  /**
7936   * Lists plugin-like directories within specified directory
7937   *
7938   * This function was originally used for standard Moodle plugins, please use
7939   * new core_component::get_plugin_list() now.
7940   *
7941   * This function is used for general directory listing and backwards compatility.
7942   *
7943   * @param string $directory relative directory from root
7944   * @param string $exclude dir name to exclude from the list (defaults to none)
7945   * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7946   * @return array Sorted array of directory names found under the requested parameters
7947   */
7948  function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7949      global $CFG;
7950  
7951      $plugins = array();
7952  
7953      if (empty($basedir)) {
7954          $basedir = $CFG->dirroot .'/'. $directory;
7955  
7956      } else {
7957          $basedir = $basedir .'/'. $directory;
7958      }
7959  
7960      if ($CFG->debugdeveloper and empty($exclude)) {
7961          // Make sure devs do not use this to list normal plugins,
7962          // this is intended for general directories that are not plugins!
7963  
7964          $subtypes = core_component::get_plugin_types();
7965          if (in_array($basedir, $subtypes)) {
7966              debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7967          }
7968          unset($subtypes);
7969      }
7970  
7971      $ignorelist = array_flip(array_filter([
7972          'CVS',
7973          '_vti_cnf',
7974          'amd',
7975          'classes',
7976          'simpletest',
7977          'tests',
7978          'templates',
7979          'yui',
7980          $exclude,
7981      ]));
7982  
7983      if (file_exists($basedir) && filetype($basedir) == 'dir') {
7984          if (!$dirhandle = opendir($basedir)) {
7985              debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7986              return array();
7987          }
7988          while (false !== ($dir = readdir($dirhandle))) {
7989              if (strpos($dir, '.') === 0) {
7990                  // Ignore directories starting with .
7991                  // These are treated as hidden directories.
7992                  continue;
7993              }
7994              if (array_key_exists($dir, $ignorelist)) {
7995                  // This directory features on the ignore list.
7996                  continue;
7997              }
7998              if (filetype($basedir .'/'. $dir) != 'dir') {
7999                  continue;
8000              }
8001              $plugins[] = $dir;
8002          }
8003          closedir($dirhandle);
8004      }
8005      if ($plugins) {
8006          asort($plugins);
8007      }
8008      return $plugins;
8009  }
8010  
8011  /**
8012   * Invoke plugin's callback functions
8013   *
8014   * @param string $type plugin type e.g. 'mod'
8015   * @param string $name plugin name
8016   * @param string $feature feature name
8017   * @param string $action feature's action
8018   * @param array $params parameters of callback function, should be an array
8019   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8020   * @return mixed
8021   *
8022   * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8023   */
8024  function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8025      return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8026  }
8027  
8028  /**
8029   * Invoke component's callback functions
8030   *
8031   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8032   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8033   * @param array $params parameters of callback function
8034   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8035   * @return mixed
8036   */
8037  function component_callback($component, $function, array $params = array(), $default = null) {
8038  
8039      $functionname = component_callback_exists($component, $function);
8040  
8041      if ($params && (array_keys($params) !== range(0, count($params) - 1))) {
8042          // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but
8043          // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions.
8044          // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array
8045          // This check can be removed when minimum PHP version for Moodle is raised to 8.
8046          debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.',
8047              DEBUG_DEVELOPER);
8048          $params = array_values($params);
8049      }
8050  
8051      if ($functionname) {
8052          // Function exists, so just return function result.
8053          $ret = call_user_func_array($functionname, $params);
8054          if (is_null($ret)) {
8055              return $default;
8056          } else {
8057              return $ret;
8058          }
8059      }
8060      return $default;
8061  }
8062  
8063  /**
8064   * Determine if a component callback exists and return the function name to call. Note that this
8065   * function will include the required library files so that the functioname returned can be
8066   * called directly.
8067   *
8068   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8069   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8070   * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8071   * @throws coding_exception if invalid component specfied
8072   */
8073  function component_callback_exists($component, $function) {
8074      global $CFG; // This is needed for the inclusions.
8075  
8076      $cleancomponent = clean_param($component, PARAM_COMPONENT);
8077      if (empty($cleancomponent)) {
8078          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8079      }
8080      $component = $cleancomponent;
8081  
8082      list($type, $name) = core_component::normalize_component($component);
8083      $component = $type . '_' . $name;
8084  
8085      $oldfunction = $name.'_'.$function;
8086      $function = $component.'_'.$function;
8087  
8088      $dir = core_component::get_component_directory($component);
8089      if (empty($dir)) {
8090          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8091      }
8092  
8093      // Load library and look for function.
8094      if (file_exists($dir.'/lib.php')) {
8095          require_once($dir.'/lib.php');
8096      }
8097  
8098      if (!function_exists($function) and function_exists($oldfunction)) {
8099          if ($type !== 'mod' and $type !== 'core') {
8100              debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8101          }
8102          $function = $oldfunction;
8103      }
8104  
8105      if (function_exists($function)) {
8106          return $function;
8107      }
8108      return false;
8109  }
8110  
8111  /**
8112   * Call the specified callback method on the provided class.
8113   *
8114   * If the callback returns null, then the default value is returned instead.
8115   * If the class does not exist, then the default value is returned.
8116   *
8117   * @param   string      $classname The name of the class to call upon.
8118   * @param   string      $methodname The name of the staticically defined method on the class.
8119   * @param   array       $params The arguments to pass into the method.
8120   * @param   mixed       $default The default value.
8121   * @return  mixed       The return value.
8122   */
8123  function component_class_callback($classname, $methodname, array $params, $default = null) {
8124      if (!class_exists($classname)) {
8125          return $default;
8126      }
8127  
8128      if (!method_exists($classname, $methodname)) {
8129          return $default;
8130      }
8131  
8132      $fullfunction = $classname . '::' . $methodname;
8133      $result = call_user_func_array($fullfunction, $params);
8134  
8135      if (null === $result) {
8136          return $default;
8137      } else {
8138          return $result;
8139      }
8140  }
8141  
8142  /**
8143   * Checks whether a plugin supports a specified feature.
8144   *
8145   * @param string $type Plugin type e.g. 'mod'
8146   * @param string $name Plugin name e.g. 'forum'
8147   * @param string $feature Feature code (FEATURE_xx constant)
8148   * @param mixed $default default value if feature support unknown
8149   * @return mixed Feature result (false if not supported, null if feature is unknown,
8150   *         otherwise usually true but may have other feature-specific value such as array)
8151   * @throws coding_exception
8152   */
8153  function plugin_supports($type, $name, $feature, $default = null) {
8154      global $CFG;
8155  
8156      if ($type === 'mod' and $name === 'NEWMODULE') {
8157          // Somebody forgot to rename the module template.
8158          return false;
8159      }
8160  
8161      $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8162      if (empty($component)) {
8163          throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8164      }
8165  
8166      $function = null;
8167  
8168      if ($type === 'mod') {
8169          // We need this special case because we support subplugins in modules,
8170          // otherwise it would end up in infinite loop.
8171          if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8172              include_once("$CFG->dirroot/mod/$name/lib.php");
8173              $function = $component.'_supports';
8174              if (!function_exists($function)) {
8175                  // Legacy non-frankenstyle function name.
8176                  $function = $name.'_supports';
8177              }
8178          }
8179  
8180      } else {
8181          if (!$path = core_component::get_plugin_directory($type, $name)) {
8182              // Non existent plugin type.
8183              return false;
8184          }
8185          if (file_exists("$path/lib.php")) {
8186              include_once("$path/lib.php");
8187              $function = $component.'_supports';
8188          }
8189      }
8190  
8191      if ($function and function_exists($function)) {
8192          $supports = $function($feature);
8193          if (is_null($supports)) {
8194              // Plugin does not know - use default.
8195              return $default;
8196          } else {
8197              return $supports;
8198          }
8199      }
8200  
8201      // Plugin does not care, so use default.
8202      return $default;
8203  }
8204  
8205  /**
8206   * Returns true if the current version of PHP is greater that the specified one.
8207   *
8208   * @todo Check PHP version being required here is it too low?
8209   *
8210   * @param string $version The version of php being tested.
8211   * @return bool
8212   */
8213  function check_php_version($version='5.2.4') {
8214      return (version_compare(phpversion(), $version) >= 0);
8215  }
8216  
8217  /**
8218   * Determine if moodle installation requires update.
8219   *
8220   * Checks version numbers of main code and all plugins to see
8221   * if there are any mismatches.
8222   *
8223   * @return bool
8224   */
8225  function moodle_needs_upgrading() {
8226      global $CFG;
8227  
8228      if (empty($CFG->version)) {
8229          return true;
8230      }
8231  
8232      // There is no need to purge plugininfo caches here because
8233      // these caches are not used during upgrade and they are purged after
8234      // every upgrade.
8235  
8236      if (empty($CFG->allversionshash)) {
8237          return true;
8238      }
8239  
8240      $hash = core_component::get_all_versions_hash();
8241  
8242      return ($hash !== $CFG->allversionshash);
8243  }
8244  
8245  /**
8246   * Returns the major version of this site
8247   *
8248   * Moodle version numbers consist of three numbers separated by a dot, for
8249   * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8250   * called major version. This function extracts the major version from either
8251   * $CFG->release (default) or eventually from the $release variable defined in
8252   * the main version.php.
8253   *
8254   * @param bool $fromdisk should the version if source code files be used
8255   * @return string|false the major version like '2.3', false if could not be determined
8256   */
8257  function moodle_major_version($fromdisk = false) {
8258      global $CFG;
8259  
8260      if ($fromdisk) {
8261          $release = null;
8262          require($CFG->dirroot.'/version.php');
8263          if (empty($release)) {
8264              return false;
8265          }
8266  
8267      } else {
8268          if (empty($CFG->release)) {
8269              return false;
8270          }
8271          $release = $CFG->release;
8272      }
8273  
8274      if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8275          return $matches[0];
8276      } else {
8277          return false;
8278      }
8279  }
8280  
8281  // MISCELLANEOUS.
8282  
8283  /**
8284   * Gets the system locale
8285   *
8286   * @return string Retuns the current locale.
8287   */
8288  function moodle_getlocale() {
8289      global $CFG;
8290  
8291      // Fetch the correct locale based on ostype.
8292      if ($CFG->ostype == 'WINDOWS') {
8293          $stringtofetch = 'localewin';
8294      } else {
8295          $stringtofetch = 'locale';
8296      }
8297  
8298      if (!empty($CFG->locale)) { // Override locale for all language packs.
8299          return $CFG->locale;
8300      }
8301  
8302      return get_string($stringtofetch, 'langconfig');
8303  }
8304  
8305  /**
8306   * Sets the system locale
8307   *
8308   * @category string
8309   * @param string $locale Can be used to force a locale
8310   */
8311  function moodle_setlocale($locale='') {
8312      global $CFG;
8313  
8314      static $currentlocale = ''; // Last locale caching.
8315  
8316      $oldlocale = $currentlocale;
8317  
8318      // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8319      if (!empty($locale)) {
8320          $currentlocale = $locale;
8321      } else {
8322          $currentlocale = moodle_getlocale();
8323      }
8324  
8325      // Do nothing if locale already set up.
8326      if ($oldlocale == $currentlocale) {
8327          return;
8328      }
8329  
8330      // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8331      // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8332      // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8333  
8334      // Get current values.
8335      $monetary= setlocale (LC_MONETARY, 0);
8336      $numeric = setlocale (LC_NUMERIC, 0);
8337      $ctype   = setlocale (LC_CTYPE, 0);
8338      if ($CFG->ostype != 'WINDOWS') {
8339          $messages= setlocale (LC_MESSAGES, 0);
8340      }
8341      // Set locale to all.
8342      $result = setlocale (LC_ALL, $currentlocale);
8343      // If setting of locale fails try the other utf8 or utf-8 variant,
8344      // some operating systems support both (Debian), others just one (OSX).
8345      if ($result === false) {
8346          if (stripos($currentlocale, '.UTF-8') !== false) {
8347              $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8348              setlocale (LC_ALL, $newlocale);
8349          } else if (stripos($currentlocale, '.UTF8') !== false) {
8350              $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8351              setlocale (LC_ALL, $newlocale);
8352          }
8353      }
8354      // Set old values.
8355      setlocale (LC_MONETARY, $monetary);
8356      setlocale (LC_NUMERIC, $numeric);
8357      if ($CFG->ostype != 'WINDOWS') {
8358          setlocale (LC_MESSAGES, $messages);
8359      }
8360      if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8361          // To workaround a well-known PHP problem with Turkish letter Ii.
8362          setlocale (LC_CTYPE, $ctype);
8363      }
8364  }
8365  
8366  /**
8367   * Count words in a string.
8368   *
8369   * Words are defined as things between whitespace.
8370   *
8371   * @category string
8372   * @param string $string The text to be searched for words. May be HTML.
8373   * @return int The count of words in the specified string
8374   */
8375  function count_words($string) {
8376      // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8377      // Also, br is a special case because it definitely delimits a word, but has no close tag.
8378      $string = preg_replace('~
8379              (                                   # Capture the tag we match.
8380                  </                              # Start of close tag.
8381                  (?!                             # Do not match any of these specific close tag names.
8382                      a> | b> | del> | em> | i> |
8383                      ins> | s> | small> | span> |
8384                      strong> | sub> | sup> | u>
8385                  )
8386                  \w+                             # But, apart from those execptions, match any tag name.
8387                  >                               # End of close tag.
8388              |
8389                  <br> | <br\s*/>                 # Special cases that are not close tags.
8390              )
8391              ~x', '$1 ', $string); // Add a space after the close tag.
8392      // Now remove HTML tags.
8393      $string = strip_tags($string);
8394      // Decode HTML entities.
8395      $string = html_entity_decode($string);
8396  
8397      // Now, the word count is the number of blocks of characters separated
8398      // by any sort of space. That seems to be the definition used by all other systems.
8399      // To be precise about what is considered to separate words:
8400      // * Anything that Unicode considers a 'Separator'
8401      // * Anything that Unicode considers a 'Control character'
8402      // * An em- or en- dash.
8403      return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8404  }
8405  
8406  /**
8407   * Count letters in a string.
8408   *
8409   * Letters are defined as chars not in tags and different from whitespace.
8410   *
8411   * @category string
8412   * @param string $string The text to be searched for letters. May be HTML.
8413   * @return int The count of letters in the specified text.
8414   */
8415  function count_letters($string) {
8416      $string = strip_tags($string); // Tags are out now.
8417      $string = html_entity_decode($string);
8418      $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8419  
8420      return core_text::strlen($string);
8421  }
8422  
8423  /**
8424   * Generate and return a random string of the specified length.
8425   *
8426   * @param int $length The length of the string to be created.
8427   * @return string
8428   */
8429  function random_string($length=15) {
8430      $randombytes = random_bytes_emulate($length);
8431      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8432      $pool .= 'abcdefghijklmnopqrstuvwxyz';
8433      $pool .= '0123456789';
8434      $poollen = strlen($pool);
8435      $string = '';
8436      for ($i = 0; $i < $length; $i++) {
8437          $rand = ord($randombytes[$i]);
8438          $string .= substr($pool, ($rand%($poollen)), 1);
8439      }
8440      return $string;
8441  }
8442  
8443  /**
8444   * Generate a complex random string (useful for md5 salts)
8445   *
8446   * This function is based on the above {@link random_string()} however it uses a
8447   * larger pool of characters and generates a string between 24 and 32 characters
8448   *
8449   * @param int $length Optional if set generates a string to exactly this length
8450   * @return string
8451   */
8452  function complex_random_string($length=null) {
8453      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8454      $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8455      $poollen = strlen($pool);
8456      if ($length===null) {
8457          $length = floor(rand(24, 32));
8458      }
8459      $randombytes = random_bytes_emulate($length);
8460      $string = '';
8461      for ($i = 0; $i < $length; $i++) {
8462          $rand = ord($randombytes[$i]);
8463          $string .= $pool[($rand%$poollen)];
8464      }
8465      return $string;
8466  }
8467  
8468  /**
8469   * Try to generates cryptographically secure pseudo-random bytes.
8470   *
8471   * Note this is achieved by fallbacking between:
8472   *  - PHP 7 random_bytes().
8473   *  - OpenSSL openssl_random_pseudo_bytes().
8474   *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
8475   *
8476   * @param int $length requested length in bytes
8477   * @return string binary data
8478   */
8479  function random_bytes_emulate($length) {
8480      global $CFG;
8481      if ($length <= 0) {
8482          debugging('Invalid random bytes length', DEBUG_DEVELOPER);
8483          return '';
8484      }
8485      if (function_exists('random_bytes')) {
8486          // Use PHP 7 goodness.
8487          $hash = @random_bytes($length);
8488          if ($hash !== false) {
8489              return $hash;
8490          }
8491      }
8492      if (function_exists('openssl_random_pseudo_bytes')) {
8493          // If you have the openssl extension enabled.
8494          $hash = openssl_random_pseudo_bytes($length);
8495          if ($hash !== false) {
8496              return $hash;
8497          }
8498      }
8499  
8500      // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess.
8501      $staticdata = serialize($CFG) . serialize($_SERVER);
8502      $hash = '';
8503      do {
8504          $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true);
8505      } while (strlen($hash) < $length);
8506  
8507      return substr($hash, 0, $length);
8508  }
8509  
8510  /**
8511   * Given some text (which may contain HTML) and an ideal length,
8512   * this function truncates the text neatly on a word boundary if possible
8513   *
8514   * @category string
8515   * @param string $text text to be shortened
8516   * @param int $ideal ideal string length
8517   * @param boolean $exact if false, $text will not be cut mid-word
8518   * @param string $ending The string to append if the passed string is truncated
8519   * @return string $truncate shortened string
8520   */
8521  function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8522      // If the plain text is shorter than the maximum length, return the whole text.
8523      if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8524          return $text;
8525      }
8526  
8527      // Splits on HTML tags. Each open/close/empty tag will be the first thing
8528      // and only tag in its 'line'.
8529      preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8530  
8531      $totallength = core_text::strlen($ending);
8532      $truncate = '';
8533  
8534      // This array stores information about open and close tags and their position
8535      // in the truncated string. Each item in the array is an object with fields
8536      // ->open (true if open), ->tag (tag name in lower case), and ->pos
8537      // (byte position in truncated text).
8538      $tagdetails = array();
8539  
8540      foreach ($lines as $linematchings) {
8541          // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8542          if (!empty($linematchings[1])) {
8543              // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8544              if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8545                  if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8546                      // Record closing tag.
8547                      $tagdetails[] = (object) array(
8548                              'open' => false,
8549                              'tag'  => core_text::strtolower($tagmatchings[1]),
8550                              'pos'  => core_text::strlen($truncate),
8551                          );
8552  
8553                  } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8554                      // Record opening tag.
8555                      $tagdetails[] = (object) array(
8556                              'open' => true,
8557                              'tag'  => core_text::strtolower($tagmatchings[1]),
8558                              'pos'  => core_text::strlen($truncate),
8559                          );
8560                  } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8561                      $tagdetails[] = (object) array(
8562                              'open' => true,
8563                              'tag'  => core_text::strtolower('if'),
8564                              'pos'  => core_text::strlen($truncate),
8565                      );
8566                  } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8567                      $tagdetails[] = (object) array(
8568                              'open' => false,
8569                              'tag'  => core_text::strtolower('if'),
8570                              'pos'  => core_text::strlen($truncate),
8571                      );
8572                  }
8573              }
8574              // Add html-tag to $truncate'd text.
8575              $truncate .= $linematchings[1];
8576          }
8577  
8578          // Calculate the length of the plain text part of the line; handle entities as one character.
8579          $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8580          if ($totallength + $contentlength > $ideal) {
8581              // The number of characters which are left.
8582              $left = $ideal - $totallength;
8583              $entitieslength = 0;
8584              // Search for html entities.
8585              if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $linematchings[2], $entities, PREG_OFFSET_CAPTURE)) {
8586                  // Calculate the real length of all entities in the legal range.
8587                  foreach ($entities[0] as $entity) {
8588                      if ($entity[1]+1-$entitieslength <= $left) {
8589                          $left--;
8590                          $entitieslength += core_text::strlen($entity[0]);
8591                      } else {
8592                          // No more characters left.
8593                          break;
8594                      }
8595                  }
8596              }
8597              $breakpos = $left + $entitieslength;
8598  
8599              // If the words shouldn't be cut in the middle...
8600              if (!$exact) {
8601                  // Search the last occurence of a space.
8602                  for (; $breakpos > 0; $breakpos--) {
8603                      if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8604                          if ($char === '.' or $char === ' ') {
8605                              $breakpos += 1;
8606                              break;
8607                          } else if (strlen($char) > 2) {
8608                              // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8609                              $breakpos += 1;
8610                              break;
8611                          }
8612                      }
8613                  }
8614              }
8615              if ($breakpos == 0) {
8616                  // This deals with the test_shorten_text_no_spaces case.
8617                  $breakpos = $left + $entitieslength;
8618              } else if ($breakpos > $left + $entitieslength) {
8619                  // This deals with the previous for loop breaking on the first char.
8620                  $breakpos = $left + $entitieslength;
8621              }
8622  
8623              $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8624              // Maximum length is reached, so get off the loop.
8625              break;
8626          } else {
8627              $truncate .= $linematchings[2];
8628              $totallength += $contentlength;
8629          }
8630  
8631          // If the maximum length is reached, get off the loop.
8632          if ($totallength >= $ideal) {
8633              break;
8634          }
8635      }
8636  
8637      // Add the defined ending to the text.
8638      $truncate .= $ending;
8639  
8640      // Now calculate the list of open html tags based on the truncate position.
8641      $opentags = array();
8642      foreach ($tagdetails as $taginfo) {
8643          if ($taginfo->open) {
8644              // Add tag to the beginning of $opentags list.
8645              array_unshift($opentags, $taginfo->tag);
8646          } else {
8647              // Can have multiple exact same open tags, close the last one.
8648              $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8649              if ($pos !== false) {
8650                  unset($opentags[$pos]);
8651              }
8652          }
8653      }
8654  
8655      // Close all unclosed html-tags.
8656      foreach ($opentags as $tag) {
8657          if ($tag === 'if') {
8658              $truncate .= '<!--<![endif]-->';
8659          } else {
8660              $truncate .= '</' . $tag . '>';
8661          }
8662      }
8663  
8664      return $truncate;
8665  }
8666  
8667  /**
8668   * Shortens a given filename by removing characters positioned after the ideal string length.
8669   * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8670   * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8671   *
8672   * @param string $filename file name
8673   * @param int $length ideal string length
8674   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8675   * @return string $shortened shortened file name
8676   */
8677  function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8678      $shortened = $filename;
8679      // Extract a part of the filename if it's char size exceeds the ideal string length.
8680      if (core_text::strlen($filename) > $length) {
8681          // Exclude extension if present in filename.
8682          $mimetypes = get_mimetypes_array();
8683          $extension = pathinfo($filename, PATHINFO_EXTENSION);
8684          if ($extension && !empty($mimetypes[$extension])) {
8685              $basename = pathinfo($filename, PATHINFO_FILENAME);
8686              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8687              $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8688              $shortened .= '.' . $extension;
8689          } else {
8690              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8691              $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8692          }
8693      }
8694      return $shortened;
8695  }
8696  
8697  /**
8698   * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8699   *
8700   * @param array $path The paths to reduce the length.
8701   * @param int $length Ideal string length
8702   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8703   * @return array $result Shortened paths in array.
8704   */
8705  function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8706      $result = null;
8707  
8708      $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8709          $carry[] = shorten_filename($singlepath, $length, $includehash);
8710          return $carry;
8711      }, []);
8712  
8713      return $result;
8714  }
8715  
8716  /**
8717   * Given dates in seconds, how many weeks is the date from startdate
8718   * The first week is 1, the second 2 etc ...
8719   *
8720   * @param int $startdate Timestamp for the start date
8721   * @param int $thedate Timestamp for the end date
8722   * @return string
8723   */
8724  function getweek ($startdate, $thedate) {
8725      if ($thedate < $startdate) {
8726          return 0;
8727      }
8728  
8729      return floor(($thedate - $startdate) / WEEKSECS) + 1;
8730  }
8731  
8732  /**
8733   * Returns a randomly generated password of length $maxlen.  inspired by
8734   *
8735   * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8736   * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8737   *
8738   * @param int $maxlen  The maximum size of the password being generated.
8739   * @return string
8740   */
8741  function generate_password($maxlen=10) {
8742      global $CFG;
8743  
8744      if (empty($CFG->passwordpolicy)) {
8745          $fillers = PASSWORD_DIGITS;
8746          $wordlist = file($CFG->wordlist);
8747          $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8748          $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8749          $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8750          $password = $word1 . $filler1 . $word2;
8751      } else {
8752          $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8753          $digits = $CFG->minpassworddigits;
8754          $lower = $CFG->minpasswordlower;
8755          $upper = $CFG->minpasswordupper;
8756          $nonalphanum = $CFG->minpasswordnonalphanum;
8757          $total = $lower + $upper + $digits + $nonalphanum;
8758          // Var minlength should be the greater one of the two ( $minlen and $total ).
8759          $minlen = $minlen < $total ? $total : $minlen;
8760          // Var maxlen can never be smaller than minlen.
8761          $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8762          $additional = $maxlen - $total;
8763  
8764          // Make sure we have enough characters to fulfill
8765          // complexity requirements.
8766          $passworddigits = PASSWORD_DIGITS;
8767          while ($digits > strlen($passworddigits)) {
8768              $passworddigits .= PASSWORD_DIGITS;
8769          }
8770          $passwordlower = PASSWORD_LOWER;
8771          while ($lower > strlen($passwordlower)) {
8772              $passwordlower .= PASSWORD_LOWER;
8773          }
8774          $passwordupper = PASSWORD_UPPER;
8775          while ($upper > strlen($passwordupper)) {
8776              $passwordupper .= PASSWORD_UPPER;
8777          }
8778          $passwordnonalphanum = PASSWORD_NONALPHANUM;
8779          while ($nonalphanum > strlen($passwordnonalphanum)) {
8780              $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8781          }
8782  
8783          // Now mix and shuffle it all.
8784          $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8785                                   substr(str_shuffle ($passwordupper), 0, $upper) .
8786                                   substr(str_shuffle ($passworddigits), 0, $digits) .
8787                                   substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8788                                   substr(str_shuffle ($passwordlower .
8789                                                       $passwordupper .
8790                                                       $passworddigits .
8791                                                       $passwordnonalphanum), 0 , $additional));
8792      }
8793  
8794      return substr ($password, 0, $maxlen);
8795  }
8796  
8797  /**
8798   * Given a float, prints it nicely.
8799   * Localized floats must not be used in calculations!
8800   *
8801   * The stripzeros feature is intended for making numbers look nicer in small
8802   * areas where it is not necessary to indicate the degree of accuracy by showing
8803   * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8804   * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8805   *
8806   * @param float $float The float to print
8807   * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8808   * @param bool $localized use localized decimal separator
8809   * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8810   *                         the decimal point are always striped if $decimalpoints is -1.
8811   * @return string locale float
8812   */
8813  function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8814      if (is_null($float)) {
8815          return '';
8816      }
8817      if ($localized) {
8818          $separator = get_string('decsep', 'langconfig');
8819      } else {
8820          $separator = '.';
8821      }
8822      if ($decimalpoints == -1) {
8823          // The following counts the number of decimals.
8824          // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8825          $floatval = floatval($float);
8826          for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8827      }
8828  
8829      $result = number_format($float, $decimalpoints, $separator, '');
8830      if ($stripzeros && $decimalpoints > 0) {
8831          // Remove zeros and final dot if not needed.
8832          // However, only do this if there is a decimal point!
8833          $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8834      }
8835      return $result;
8836  }
8837  
8838  /**
8839   * Converts locale specific floating point/comma number back to standard PHP float value
8840   * Do NOT try to do any math operations before this conversion on any user submitted floats!
8841   *
8842   * @param string $localefloat locale aware float representation
8843   * @param bool $strict If true, then check the input and return false if it is not a valid number.
8844   * @return mixed float|bool - false or the parsed float.
8845   */
8846  function unformat_float($localefloat, $strict = false) {
8847      $localefloat = trim($localefloat);
8848  
8849      if ($localefloat == '') {
8850          return null;
8851      }
8852  
8853      $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8854      $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8855  
8856      if ($strict && !is_numeric($localefloat)) {
8857          return false;
8858      }
8859  
8860      return (float)$localefloat;
8861  }
8862  
8863  /**
8864   * Given a simple array, this shuffles it up just like shuffle()
8865   * Unlike PHP's shuffle() this function works on any machine.
8866   *
8867   * @param array $array The array to be rearranged
8868   * @return array
8869   */
8870  function swapshuffle($array) {
8871  
8872      $last = count($array) - 1;
8873      for ($i = 0; $i <= $last; $i++) {
8874          $from = rand(0, $last);
8875          $curr = $array[$i];
8876          $array[$i] = $array[$from];
8877          $array[$from] = $curr;
8878      }
8879      return $array;
8880  }
8881  
8882  /**
8883   * Like {@link swapshuffle()}, but works on associative arrays
8884   *
8885   * @param array $array The associative array to be rearranged
8886   * @return array
8887   */
8888  function swapshuffle_assoc($array) {
8889  
8890      $newarray = array();
8891      $newkeys = swapshuffle(array_keys($array));
8892  
8893      foreach ($newkeys as $newkey) {
8894          $newarray[$newkey] = $array[$newkey];
8895      }
8896      return $newarray;
8897  }
8898  
8899  /**
8900   * Given an arbitrary array, and a number of draws,
8901   * this function returns an array with that amount
8902   * of items.  The indexes are retained.
8903   *
8904   * @todo Finish documenting this function
8905   *
8906   * @param array $array
8907   * @param int $draws
8908   * @return array
8909   */
8910  function draw_rand_array($array, $draws) {
8911  
8912      $return = array();
8913  
8914      $last = count($array);
8915  
8916      if ($draws > $last) {
8917          $draws = $last;
8918      }
8919  
8920      while ($draws > 0) {
8921          $last--;
8922  
8923          $keys = array_keys($array);
8924          $rand = rand(0, $last);
8925  
8926          $return[$keys[$rand]] = $array[$keys[$rand]];
8927          unset($array[$keys[$rand]]);
8928  
8929          $draws--;
8930      }
8931  
8932      return $return;
8933  }
8934  
8935  /**
8936   * Calculate the difference between two microtimes
8937   *
8938   * @param string $a The first Microtime
8939   * @param string $b The second Microtime
8940   * @return string
8941   */
8942  function microtime_diff($a, $b) {
8943      list($adec, $asec) = explode(' ', $a);
8944      list($bdec, $bsec) = explode(' ', $b);
8945      return $bsec - $asec + $bdec - $adec;
8946  }
8947  
8948  /**
8949   * Given a list (eg a,b,c,d,e) this function returns
8950   * an array of 1->a, 2->b, 3->c etc
8951   *
8952   * @param string $list The string to explode into array bits
8953   * @param string $separator The separator used within the list string
8954   * @return array The now assembled array
8955   */
8956  function make_menu_from_list($list, $separator=',') {
8957  
8958      $array = array_reverse(explode($separator, $list), true);
8959      foreach ($array as $key => $item) {
8960          $outarray[$key+1] = trim($item);
8961      }
8962      return $outarray;
8963  }
8964  
8965  /**
8966   * Creates an array that represents all the current grades that
8967   * can be chosen using the given grading type.
8968   *
8969   * Negative numbers
8970   * are scales, zero is no grade, and positive numbers are maximum
8971   * grades.
8972   *
8973   * @todo Finish documenting this function or better deprecated this completely!
8974   *
8975   * @param int $gradingtype
8976   * @return array
8977   */
8978  function make_grades_menu($gradingtype) {
8979      global $DB;
8980  
8981      $grades = array();
8982      if ($gradingtype < 0) {
8983          if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8984              return make_menu_from_list($scale->scale);
8985          }
8986      } else if ($gradingtype > 0) {
8987          for ($i=$gradingtype; $i>=0; $i--) {
8988              $grades[$i] = $i .' / '. $gradingtype;
8989          }
8990          return $grades;
8991      }
8992      return $grades;
8993  }
8994  
8995  /**
8996   * make_unique_id_code
8997   *
8998   * @todo Finish documenting this function
8999   *
9000   * @uses $_SERVER
9001   * @param string $extra Extra string to append to the end of the code
9002   * @return string
9003   */
9004  function make_unique_id_code($extra = '') {
9005  
9006      $hostname = 'unknownhost';
9007      if (!empty($_SERVER['HTTP_HOST'])) {
9008          $hostname = $_SERVER['HTTP_HOST'];
9009      } else if (!empty($_ENV['HTTP_HOST'])) {
9010          $hostname = $_ENV['HTTP_HOST'];
9011      } else if (!empty($_SERVER['SERVER_NAME'])) {
9012          $hostname = $_SERVER['SERVER_NAME'];
9013      } else if (!empty($_ENV['SERVER_NAME'])) {
9014          $hostname = $_ENV['SERVER_NAME'];
9015      }
9016  
9017      $date = gmdate("ymdHis");
9018  
9019      $random =  random_string(6);
9020  
9021      if ($extra) {
9022          return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9023      } else {
9024          return $hostname .'+'. $date .'+'. $random;
9025      }
9026  }
9027  
9028  
9029  /**
9030   * Function to check the passed address is within the passed subnet
9031   *
9032   * The parameter is a comma separated string of subnet definitions.
9033   * Subnet strings can be in one of three formats:
9034   *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
9035   *   2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy (a range of IP addresses in the last group)
9036   *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
9037   * Code for type 1 modified from user posted comments by mediator at
9038   * {@link http://au.php.net/manual/en/function.ip2long.php}
9039   *
9040   * @param string $addr    The address you are checking
9041   * @param string $subnetstr    The string of subnet addresses
9042   * @param bool $checkallzeros    The state to whether check for 0.0.0.0
9043   * @return bool
9044   */
9045  function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
9046  
9047      if ($addr == '0.0.0.0' && !$checkallzeros) {
9048          return false;
9049      }
9050      $subnets = explode(',', $subnetstr);
9051      $found = false;
9052      $addr = trim($addr);
9053      $addr = cleanremoteaddr($addr, false); // Normalise.
9054      if ($addr === null) {
9055          return false;
9056      }
9057      $addrparts = explode(':', $addr);
9058  
9059      $ipv6 = strpos($addr, ':');
9060  
9061      foreach ($subnets as $subnet) {
9062          $subnet = trim($subnet);
9063          if ($subnet === '') {
9064              continue;
9065          }
9066  
9067          if (strpos($subnet, '/') !== false) {
9068              // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9069              list($ip, $mask) = explode('/', $subnet);
9070              $mask = trim($mask);
9071              if (!is_number($mask)) {
9072                  continue; // Incorect mask number, eh?
9073              }
9074              $ip = cleanremoteaddr($ip, false); // Normalise.
9075              if ($ip === null) {
9076                  continue;
9077              }
9078              if (strpos($ip, ':') !== false) {
9079                  // IPv6.
9080                  if (!$ipv6) {
9081                      continue;
9082                  }
9083                  if ($mask > 128 or $mask < 0) {
9084                      continue; // Nonsense.
9085                  }
9086                  if ($mask == 0) {
9087                      return true; // Any address.
9088                  }
9089                  if ($mask == 128) {
9090                      if ($ip === $addr) {
9091                          return true;
9092                      }
9093                      continue;
9094                  }
9095                  $ipparts = explode(':', $ip);
9096                  $modulo  = $mask % 16;
9097                  $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
9098                  $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9099                  if (implode(':', $ipnet) === implode(':', $addrnet)) {
9100                      if ($modulo == 0) {
9101                          return true;
9102                      }
9103                      $pos     = ($mask-$modulo)/16;
9104                      $ipnet   = hexdec($ipparts[$pos]);
9105                      $addrnet = hexdec($addrparts[$pos]);
9106                      $mask    = 0xffff << (16 - $modulo);
9107                      if (($addrnet & $mask) == ($ipnet & $mask)) {
9108                          return true;
9109                      }
9110                  }
9111  
9112              } else {
9113                  // IPv4.
9114                  if ($ipv6) {
9115                      continue;
9116                  }
9117                  if ($mask > 32 or $mask < 0) {
9118                      continue; // Nonsense.
9119                  }
9120                  if ($mask == 0) {
9121                      return true;
9122                  }
9123                  if ($mask == 32) {
9124                      if ($ip === $addr) {
9125                          return true;
9126                      }
9127                      continue;
9128                  }
9129                  $mask = 0xffffffff << (32 - $mask);
9130                  if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9131                      return true;
9132                  }
9133              }
9134  
9135          } else if (strpos($subnet, '-') !== false) {
9136              // 2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy. A range of IP addresses in the last group.
9137              $parts = explode('-', $subnet);
9138              if (count($parts) != 2) {
9139                  continue;
9140              }
9141  
9142              if (strpos($subnet, ':') !== false) {
9143                  // IPv6.
9144                  if (!$ipv6) {
9145                      continue;
9146                  }
9147                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9148                  if ($ipstart === null) {
9149                      continue;
9150                  }
9151                  $ipparts = explode(':', $ipstart);
9152                  $start = hexdec(array_pop($ipparts));
9153                  $ipparts[] = trim($parts[1]);
9154                  $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9155                  if ($ipend === null) {
9156                      continue;
9157                  }
9158                  $ipparts[7] = '';
9159                  $ipnet = implode(':', $ipparts);
9160                  if (strpos($addr, $ipnet) !== 0) {
9161                      continue;
9162                  }
9163                  $ipparts = explode(':', $ipend);
9164                  $end = hexdec($ipparts[7]);
9165  
9166                  $addrend = hexdec($addrparts[7]);
9167  
9168                  if (($addrend >= $start) and ($addrend <= $end)) {
9169                      return true;
9170                  }
9171  
9172              } else {
9173                  // IPv4.
9174                  if ($ipv6) {
9175                      continue;
9176                  }
9177                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9178                  if ($ipstart === null) {
9179                      continue;
9180                  }
9181                  $ipparts = explode('.', $ipstart);
9182                  $ipparts[3] = trim($parts[1]);
9183                  $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9184                  if ($ipend === null) {
9185                      continue;
9186                  }
9187  
9188                  if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9189                      return true;
9190                  }
9191              }
9192  
9193          } else {
9194              // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9195              if (strpos($subnet, ':') !== false) {
9196                  // IPv6.
9197                  if (!$ipv6) {
9198                      continue;
9199                  }
9200                  $parts = explode(':', $subnet);
9201                  $count = count($parts);
9202                  if ($parts[$count-1] === '') {
9203                      unset($parts[$count-1]); // Trim trailing :'s.
9204                      $count--;
9205                      $subnet = implode('.', $parts);
9206                  }
9207                  $isip = cleanremoteaddr($subnet, false); // Normalise.
9208                  if ($isip !== null) {
9209                      if ($isip === $addr) {
9210                          return true;
9211                      }
9212                      continue;
9213                  } else if ($count > 8) {
9214                      continue;
9215                  }
9216                  $zeros = array_fill(0, 8-$count, '0');
9217                  $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9218                  if (address_in_subnet($addr, $subnet)) {
9219                      return true;
9220                  }
9221  
9222              } else {
9223                  // IPv4.
9224                  if ($ipv6) {
9225                      continue;
9226                  }
9227                  $parts = explode('.', $subnet);
9228                  $count = count($parts);
9229                  if ($parts[$count-1] === '') {
9230                      unset($parts[$count-1]); // Trim trailing .
9231                      $count--;
9232                      $subnet = implode('.', $parts);
9233                  }
9234                  if ($count == 4) {
9235                      $subnet = cleanremoteaddr($subnet, false); // Normalise.
9236                      if ($subnet === $addr) {
9237                          return true;
9238                      }
9239                      continue;
9240                  } else if ($count > 4) {
9241                      continue;
9242                  }
9243                  $zeros = array_fill(0, 4-$count, '0');
9244                  $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9245                  if (address_in_subnet($addr, $subnet)) {
9246                      return true;
9247                  }
9248              }
9249          }
9250      }
9251  
9252      return false;
9253  }
9254  
9255  /**
9256   * For outputting debugging info
9257   *
9258   * @param string $string The string to write
9259   * @param string $eol The end of line char(s) to use
9260   * @param string $sleep Period to make the application sleep
9261   *                      This ensures any messages have time to display before redirect
9262   */
9263  function mtrace($string, $eol="\n", $sleep=0) {
9264      global $CFG;
9265  
9266      if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9267          $fn = $CFG->mtrace_wrapper;
9268          $fn($string, $eol);
9269          return;
9270      } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9271          // We must explicitly call the add_line function here.
9272          // Uses of fwrite to STDOUT are not picked up by ob_start.
9273          if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9274              fwrite(STDOUT, $output);
9275          }
9276      } else {
9277          echo $string . $eol;
9278      }
9279  
9280      // Flush again.
9281      flush();
9282  
9283      // Delay to keep message on user's screen in case of subsequent redirect.
9284      if ($sleep) {
9285          sleep($sleep);
9286      }
9287  }
9288  
9289  /**
9290   * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9291   *
9292   * @param Throwable $e the error to ouptput.
9293   */
9294  function mtrace_exception(Throwable $e): void {
9295      $info = get_exception_info($e);
9296  
9297      $message = $info->message;
9298      if ($info->debuginfo) {
9299          $message .= "\n\n" . $info->debuginfo;
9300      }
9301      if ($info->backtrace) {
9302          $message .= "\n\n" . format_backtrace($info->backtrace, true);
9303      }
9304  
9305      mtrace($message);
9306  }
9307  
9308  /**
9309   * Replace 1 or more slashes or backslashes to 1 slash
9310   *
9311   * @param string $path The path to strip
9312   * @return string the path with double slashes removed
9313   */
9314  function cleardoubleslashes ($path) {
9315      return preg_replace('/(\/|\\\){1,}/', '/', $path);
9316  }
9317  
9318  /**
9319   * Is the current ip in a given list?
9320   *
9321   * @param string $list
9322   * @return bool
9323   */
9324  function remoteip_in_list($list) {
9325      $clientip = getremoteaddr(null);
9326  
9327      if (!$clientip) {
9328          // Ensure access on cli.
9329          return true;
9330      }
9331      return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9332  }
9333  
9334  /**
9335   * Returns most reliable client address
9336   *
9337   * @param string $default If an address can't be determined, then return this
9338   * @return string The remote IP address
9339   */
9340  function getremoteaddr($default='0.0.0.0') {
9341      global $CFG;
9342  
9343      if (!isset($CFG->getremoteaddrconf)) {
9344          // This will happen, for example, before just after the upgrade, as the
9345          // user is redirected to the admin screen.
9346          $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9347      } else {
9348          $variablestoskip = $CFG->getremoteaddrconf;
9349      }
9350      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9351          if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9352              $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9353              return $address ? $address : $default;
9354          }
9355      }
9356      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9357          if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9358              $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9359  
9360              $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9361                  global $CFG;
9362                  return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9363              });
9364  
9365              // Multiple proxies can append values to this header including an
9366              // untrusted original request header so we must only trust the last ip.
9367              $address = end($forwardedaddresses);
9368  
9369              if (substr_count($address, ":") > 1) {
9370                  // Remove port and brackets from IPv6.
9371                  if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9372                      $address = $matches[1];
9373                  }
9374              } else {
9375                  // Remove port from IPv4.
9376                  if (substr_count($address, ":") == 1) {
9377                      $parts = explode(":", $address);
9378                      $address = $parts[0];
9379                  }
9380              }
9381  
9382              $address = cleanremoteaddr($address);
9383              return $address ? $address : $default;
9384          }
9385      }
9386      if (!empty($_SERVER['REMOTE_ADDR'])) {
9387          $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9388          return $address ? $address : $default;
9389      } else {
9390          return $default;
9391      }
9392  }
9393  
9394  /**
9395   * Cleans an ip address. Internal addresses are now allowed.
9396   * (Originally local addresses were not allowed.)
9397   *
9398   * @param string $addr IPv4 or IPv6 address
9399   * @param bool $compress use IPv6 address compression
9400   * @return string normalised ip address string, null if error
9401   */
9402  function cleanremoteaddr($addr, $compress=false) {
9403      $addr = trim($addr);
9404  
9405      if (strpos($addr, ':') !== false) {
9406          // Can be only IPv6.
9407          $parts = explode(':', $addr);
9408          $count = count($parts);
9409  
9410          if (strpos($parts[$count-1], '.') !== false) {
9411              // Legacy ipv4 notation.
9412              $last = array_pop($parts);
9413              $ipv4 = cleanremoteaddr($last, true);
9414              if ($ipv4 === null) {
9415                  return null;
9416              }
9417              $bits = explode('.', $ipv4);
9418              $parts[] = dechex($bits[0]).dechex($bits[1]);
9419              $parts[] = dechex($bits[2]).dechex($bits[3]);
9420              $count = count($parts);
9421              $addr = implode(':', $parts);
9422          }
9423  
9424          if ($count < 3 or $count > 8) {
9425              return null; // Severly malformed.
9426          }
9427  
9428          if ($count != 8) {
9429              if (strpos($addr, '::') === false) {
9430                  return null; // Malformed.
9431              }
9432              // Uncompress.
9433              $insertat = array_search('', $parts, true);
9434              $missing = array_fill(0, 1 + 8 - $count, '0');
9435              array_splice($parts, $insertat, 1, $missing);
9436              foreach ($parts as $key => $part) {
9437                  if ($part === '') {
9438                      $parts[$key] = '0';
9439                  }
9440              }
9441          }
9442  
9443          $adr = implode(':', $parts);
9444          if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9445              return null; // Incorrect format - sorry.
9446          }
9447  
9448          // Normalise 0s and case.
9449          $parts = array_map('hexdec', $parts);
9450          $parts = array_map('dechex', $parts);
9451  
9452          $result = implode(':', $parts);
9453  
9454          if (!$compress) {
9455              return $result;
9456          }
9457  
9458          if ($result === '0:0:0:0:0:0:0:0') {
9459              return '::'; // All addresses.
9460          }
9461  
9462          $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9463          if ($compressed !== $result) {
9464              return $compressed;
9465          }
9466  
9467          $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9468          if ($compressed !== $result) {
9469              return $compressed;
9470          }
9471  
9472          $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9473          if ($compressed !== $result) {
9474              return $compressed;
9475          }
9476  
9477          return $result;
9478      }
9479  
9480      // First get all things that look like IPv4 addresses.
9481      $parts = array();
9482      if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9483          return null;
9484      }
9485      unset($parts[0]);
9486  
9487      foreach ($parts as $key => $match) {
9488          if ($match > 255) {
9489              return null;
9490          }
9491          $parts[$key] = (int)$match; // Normalise 0s.
9492      }
9493  
9494      return implode('.', $parts);
9495  }
9496  
9497  
9498  /**
9499   * Is IP address a public address?
9500   *
9501   * @param string $ip The ip to check
9502   * @return bool true if the ip is public
9503   */
9504  function ip_is_public($ip) {
9505      return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9506  }
9507  
9508  /**
9509   * This function will make a complete copy of anything it's given,
9510   * regardless of whether it's an object or not.
9511   *
9512   * @param mixed $thing Something you want cloned
9513   * @return mixed What ever it is you passed it
9514   */
9515  function fullclone($thing) {
9516      return unserialize(serialize($thing));
9517  }
9518  
9519  /**
9520   * Used to make sure that $min <= $value <= $max
9521   *
9522   * Make sure that value is between min, and max
9523   *
9524   * @param int $min The minimum value
9525   * @param int $value The value to check
9526   * @param int $max The maximum value
9527   * @return int
9528   */
9529  function bounded_number($min, $value, $max) {
9530      if ($value < $min) {
9531          return $min;
9532      }
9533      if ($value > $max) {
9534          return $max;
9535      }
9536      return $value;
9537  }
9538  
9539  /**
9540   * Check if there is a nested array within the passed array
9541   *
9542   * @param array $array
9543   * @return bool true if there is a nested array false otherwise
9544   */
9545  function array_is_nested($array) {
9546      foreach ($array as $value) {
9547          if (is_array($value)) {
9548              return true;
9549          }
9550      }
9551      return false;
9552  }
9553  
9554  /**
9555   * get_performance_info() pairs up with init_performance_info()
9556   * loaded in setup.php. Returns an array with 'html' and 'txt'
9557   * values ready for use, and each of the individual stats provided
9558   * separately as well.
9559   *
9560   * @return array
9561   */
9562  function get_performance_info() {
9563      global $CFG, $PERF, $DB, $PAGE;
9564  
9565      $info = array();
9566      $info['txt']  = me() . ' '; // Holds log-friendly representation.
9567  
9568      $info['html'] = '';
9569      if (!empty($CFG->themedesignermode)) {
9570          // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9571          $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9572      }
9573      $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9574  
9575      $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9576  
9577      $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9578      $info['txt'] .= 'time: '.$info['realtime'].'s ';
9579  
9580      // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9581      $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9582  
9583      if (function_exists('memory_get_usage')) {
9584          $info['memory_total'] = memory_get_usage();
9585          $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9586          $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9587          $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9588              $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9589      }
9590  
9591      if (function_exists('memory_get_peak_usage')) {
9592          $info['memory_peak'] = memory_get_peak_usage();
9593          $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9594          $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9595      }
9596  
9597      $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9598      $inc = get_included_files();
9599      $info['includecount'] = count($inc);
9600      $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9601      $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9602  
9603      if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9604          // We can not track more performance before installation or before PAGE init, sorry.
9605          return $info;
9606      }
9607  
9608      $filtermanager = filter_manager::instance();
9609      if (method_exists($filtermanager, 'get_performance_summary')) {
9610          list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9611          $info = array_merge($filterinfo, $info);
9612          foreach ($filterinfo as $key => $value) {
9613              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9614              $info['txt'] .= "$key: $value ";
9615          }
9616      }
9617  
9618      $stringmanager = get_string_manager();
9619      if (method_exists($stringmanager, 'get_performance_summary')) {
9620          list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9621          $info = array_merge($filterinfo, $info);
9622          foreach ($filterinfo as $key => $value) {
9623              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9624              $info['txt'] .= "$key: $value ";
9625          }
9626      }
9627  
9628      if (!empty($PERF->logwrites)) {
9629          $info['logwrites'] = $PERF->logwrites;
9630          $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> ';
9631          $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9632      }
9633  
9634      $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9635      $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9636      $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9637  
9638      if ($DB->want_read_slave()) {
9639          $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9640          $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9641          $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9642      }
9643  
9644      $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9645      $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9646      $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9647  
9648      if (function_exists('posix_times')) {
9649          $ptimes = posix_times();
9650          if (is_array($ptimes)) {
9651              foreach ($ptimes as $key => $val) {
9652                  $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9653              }
9654              $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9655              $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9656              $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9657          }
9658      }
9659  
9660      // Grab the load average for the last minute.
9661      // /proc will only work under some linux configurations
9662      // while uptime is there under MacOSX/Darwin and other unices.
9663      if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9664          list($serverload) = explode(' ', $loadavg[0]);
9665          unset($loadavg);
9666      } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9667          if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9668              $serverload = $matches[1];
9669          } else {
9670              trigger_error('Could not parse uptime output!');
9671          }
9672      }
9673      if (!empty($serverload)) {
9674          $info['serverload'] = $serverload;
9675          $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9676          $info['txt'] .= "serverload: {$info['serverload']} ";
9677      }
9678  
9679      // Display size of session if session started.
9680      if ($si = \core\session\manager::get_performance_info()) {
9681          $info['sessionsize'] = $si['size'];
9682          $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9683          $info['txt'] .= $si['txt'];
9684      }
9685  
9686      $info['html'] .= '</ul>';
9687      $html = '';
9688      if ($stats = cache_helper::get_stats()) {
9689  
9690          $table = new html_table();
9691          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9692          $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9693          $table->data = [];
9694          $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9695  
9696          $text = 'Caches used (hits/misses/sets): ';
9697          $hits = 0;
9698          $misses = 0;
9699          $sets = 0;
9700          $maxstores = 0;
9701  
9702          // We want to align static caches into their own column.
9703          $hasstatic = false;
9704          foreach ($stats as $definition => $details) {
9705              $numstores = count($details['stores']);
9706              $first = key($details['stores']);
9707              if ($first !== cache_store::STATIC_ACCEL) {
9708                  $numstores++; // Add a blank space for the missing static store.
9709              }
9710              $maxstores = max($maxstores, $numstores);
9711          }
9712  
9713          $storec = 0;
9714  
9715          while ($storec++ < ($maxstores - 2)) {
9716              if ($storec == ($maxstores - 2)) {
9717                  $table->head[] = get_string('mappingfinal', 'cache');
9718              } else {
9719                  $table->head[] = "Store $storec";
9720              }
9721              $table->align[] = 'left';
9722              $table->align[] = 'right';
9723              $table->align[] = 'right';
9724              $table->align[] = 'right';
9725              $table->align[] = 'right';
9726              $table->head[] = 'H';
9727              $table->head[] = 'M';
9728              $table->head[] = 'S';
9729              $table->head[] = 'I/O';
9730          }
9731  
9732          ksort($stats);
9733  
9734          foreach ($stats as $definition => $details) {
9735              switch ($details['mode']) {
9736                  case cache_store::MODE_APPLICATION:
9737                      $modeclass = 'application';
9738                      $mode = ' <span title="application cache">App</span>';
9739                      break;
9740                  case cache_store::MODE_SESSION:
9741                      $modeclass = 'session';
9742                      $mode = ' <span title="session cache">Ses</span>';
9743                      break;
9744                  case cache_store::MODE_REQUEST:
9745                      $modeclass = 'request';
9746                      $mode = ' <span title="request cache">Req</span>';
9747                      break;
9748              }
9749              $row = [$mode, $definition];
9750  
9751              $text .= "$definition {";
9752  
9753              $storec = 0;
9754              foreach ($details['stores'] as $store => $data) {
9755  
9756                  if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9757                      $row[] = '';
9758                      $row[] = '';
9759                      $row[] = '';
9760                      $storec++;
9761                  }
9762  
9763                  $hits   += $data['hits'];
9764                  $misses += $data['misses'];
9765                  $sets   += $data['sets'];
9766                  if ($data['hits'] == 0 and $data['misses'] > 0) {
9767                      $cachestoreclass = 'nohits bg-danger';
9768                  } else if ($data['hits'] < $data['misses']) {
9769                      $cachestoreclass = 'lowhits bg-warning text-dark';
9770                  } else {
9771                      $cachestoreclass = 'hihits';
9772                  }
9773                  $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9774                  $cell = new html_table_cell($store);
9775                  $cell->attributes = ['class' => $cachestoreclass];
9776                  $row[] = $cell;
9777                  $cell = new html_table_cell($data['hits']);
9778                  $cell->attributes = ['class' => $cachestoreclass];
9779                  $row[] = $cell;
9780                  $cell = new html_table_cell($data['misses']);
9781                  $cell->attributes = ['class' => $cachestoreclass];
9782                  $row[] = $cell;
9783  
9784                  if ($store !== cache_store::STATIC_ACCEL) {
9785                      // The static cache is never set.
9786                      $cell = new html_table_cell($data['sets']);
9787                      $cell->attributes = ['class' => $cachestoreclass];
9788                      $row[] = $cell;
9789  
9790                      if ($data['hits'] || $data['sets']) {
9791                          if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9792                              $size = '-';
9793                          } else {
9794                              $size = display_size($data['iobytes'], 1, 'KB');
9795                              if ($data['iobytes'] >= 10 * 1024) {
9796                                  $cachestoreclass = ' bg-warning text-dark';
9797                              }
9798                          }
9799                      } else {
9800                          $size = '';
9801                      }
9802                      $cell = new html_table_cell($size);
9803                      $cell->attributes = ['class' => $cachestoreclass];
9804                      $row[] = $cell;
9805                  }
9806                  $storec++;
9807              }
9808              while ($storec++ < $maxstores) {
9809                  $row[] = '';
9810                  $row[] = '';
9811                  $row[] = '';
9812                  $row[] = '';
9813                  $row[] = '';
9814              }
9815              $text .= '} ';
9816  
9817              $table->data[] = $row;
9818          }
9819  
9820          $html .= html_writer::table($table);
9821  
9822          // Now lets also show sub totals for each cache store.
9823          $storetotals = [];
9824          $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9825          foreach ($stats as $definition => $details) {
9826              foreach ($details['stores'] as $store => $data) {
9827                  if (!array_key_exists($store, $storetotals)) {
9828                      $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9829                  }
9830                  $storetotals[$store]['class']   = $data['class'];
9831                  $storetotals[$store]['hits']   += $data['hits'];
9832                  $storetotals[$store]['misses'] += $data['misses'];
9833                  $storetotals[$store]['sets']   += $data['sets'];
9834                  $storetotal['hits']   += $data['hits'];
9835                  $storetotal['misses'] += $data['misses'];
9836                  $storetotal['sets']   += $data['sets'];
9837                  if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9838                      $storetotals[$store]['iobytes'] += $data['iobytes'];
9839                      $storetotal['iobytes'] += $data['iobytes'];
9840                  }
9841              }
9842          }
9843  
9844          $table = new html_table();
9845          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9846          $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9847          $table->data = [];
9848          $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9849  
9850          ksort($storetotals);
9851  
9852          foreach ($storetotals as $store => $data) {
9853              $row = [];
9854              if ($data['hits'] == 0 and $data['misses'] > 0) {
9855                  $cachestoreclass = 'nohits bg-danger';
9856              } else if ($data['hits'] < $data['misses']) {
9857                  $cachestoreclass = 'lowhits bg-warning text-dark';
9858              } else {
9859                  $cachestoreclass = 'hihits';
9860              }
9861              $cell = new html_table_cell($store);
9862              $cell->attributes = ['class' => $cachestoreclass];
9863              $row[] = $cell;
9864              $cell = new html_table_cell($data['class']);
9865              $cell->attributes = ['class' => $cachestoreclass];
9866              $row[] = $cell;
9867              $cell = new html_table_cell($data['hits']);
9868              $cell->attributes = ['class' => $cachestoreclass];
9869              $row[] = $cell;
9870              $cell = new html_table_cell($data['misses']);
9871              $cell->attributes = ['class' => $cachestoreclass];
9872              $row[] = $cell;
9873              $cell = new html_table_cell($data['sets']);
9874              $cell->attributes = ['class' => $cachestoreclass];
9875              $row[] = $cell;
9876              if ($data['hits'] || $data['sets']) {
9877                  if ($data['iobytes']) {
9878                      $size = display_size($data['iobytes'], 1, 'KB');
9879                  } else {
9880                      $size = '-';
9881                  }
9882              } else {
9883                  $size = '';
9884              }
9885              $cell = new html_table_cell($size);
9886              $cell->attributes = ['class' => $cachestoreclass];
9887              $row[] = $cell;
9888              $table->data[] = $row;
9889          }
9890          if (!empty($storetotal['iobytes'])) {
9891              $size = display_size($storetotal['iobytes'], 1, 'KB');
9892          } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9893              $size = '-';
9894          } else {
9895              $size = '';
9896          }
9897          $row = [
9898              get_string('total'),
9899              '',
9900              $storetotal['hits'],
9901              $storetotal['misses'],
9902              $storetotal['sets'],
9903              $size,
9904          ];
9905          $table->data[] = $row;
9906  
9907          $html .= html_writer::table($table);
9908  
9909          $info['cachesused'] = "$hits / $misses / $sets";
9910          $info['html'] .= $html;
9911          $info['txt'] .= $text.'. ';
9912      } else {
9913          $info['cachesused'] = '0 / 0 / 0';
9914          $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9915          $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9916      }
9917  
9918      $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
9919      return $info;
9920  }
9921  
9922  /**
9923   * Renames a file or directory to a unique name within the same directory.
9924   *
9925   * This function is designed to avoid any potential race conditions, and select an unused name.
9926   *
9927   * @param string $filepath Original filepath
9928   * @param string $prefix Prefix to use for the temporary name
9929   * @return string|bool New file path or false if failed
9930   * @since Moodle 3.10
9931   */
9932  function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9933      $dir = dirname($filepath);
9934      $basename = $dir . '/' . $prefix;
9935      $limit = 0;
9936      while ($limit < 100) {
9937          // Select a new name based on a random number.
9938          $newfilepath = $basename . md5(mt_rand());
9939  
9940          // Attempt a rename to that new name.
9941          if (@rename($filepath, $newfilepath)) {
9942              return $newfilepath;
9943          }
9944  
9945          // The first time, do some sanity checks, maybe it is failing for a good reason and there
9946          // is no point trying 100 times if so.
9947          if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9948              return false;
9949          }
9950          $limit++;
9951      }
9952      return false;
9953  }
9954  
9955  /**
9956   * Delete directory or only its content
9957   *
9958   * @param string $dir directory path
9959   * @param bool $contentonly
9960   * @return bool success, true also if dir does not exist
9961   */
9962  function remove_dir($dir, $contentonly=false) {
9963      if (!is_dir($dir)) {
9964          // Nothing to do.
9965          return true;
9966      }
9967  
9968      if (!$contentonly) {
9969          // Start by renaming the directory; this will guarantee that other processes don't write to it
9970          // while it is in the process of being deleted.
9971          $tempdir = rename_to_unused_name($dir);
9972          if ($tempdir) {
9973              // If the rename was successful then delete the $tempdir instead.
9974              $dir = $tempdir;
9975          }
9976          // If the rename fails, we will continue through and attempt to delete the directory
9977          // without renaming it since that is likely to at least delete most of the files.
9978      }
9979  
9980      if (!$handle = opendir($dir)) {
9981          return false;
9982      }
9983      $result = true;
9984      while (false!==($item = readdir($handle))) {
9985          if ($item != '.' && $item != '..') {
9986              if (is_dir($dir.'/'.$item)) {
9987                  $result = remove_dir($dir.'/'.$item) && $result;
9988              } else {
9989                  $result = unlink($dir.'/'.$item) && $result;
9990              }
9991          }
9992      }
9993      closedir($handle);
9994      if ($contentonly) {
9995          clearstatcache(); // Make sure file stat cache is properly invalidated.
9996          return $result;
9997      }
9998      $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9999      clearstatcache(); // Make sure file stat cache is properly invalidated.
10000      return $result;
10001  }
10002  
10003  /**
10004   * Detect if an object or a class contains a given property
10005   * will take an actual object or the name of a class
10006   *
10007   * @param mix $obj Name of class or real object to test
10008   * @param string $property name of property to find
10009   * @return bool true if property exists
10010   */
10011  function object_property_exists( $obj, $property ) {
10012      if (is_string( $obj )) {
10013          $properties = get_class_vars( $obj );
10014      } else {
10015          $properties = get_object_vars( $obj );
10016      }
10017      return array_key_exists( $property, $properties );
10018  }
10019  
10020  /**
10021   * Converts an object into an associative array
10022   *
10023   * This function converts an object into an associative array by iterating
10024   * over its public properties. Because this function uses the foreach
10025   * construct, Iterators are respected. It works recursively on arrays of objects.
10026   * Arrays and simple values are returned as is.
10027   *
10028   * If class has magic properties, it can implement IteratorAggregate
10029   * and return all available properties in getIterator()
10030   *
10031   * @param mixed $var
10032   * @return array
10033   */
10034  function convert_to_array($var) {
10035      $result = array();
10036  
10037      // Loop over elements/properties.
10038      foreach ($var as $key => $value) {
10039          // Recursively convert objects.
10040          if (is_object($value) || is_array($value)) {
10041              $result[$key] = convert_to_array($value);
10042          } else {
10043              // Simple values are untouched.
10044              $result[$key] = $value;
10045          }
10046      }
10047      return $result;
10048  }
10049  
10050  /**
10051   * Detect a custom script replacement in the data directory that will
10052   * replace an existing moodle script
10053   *
10054   * @return string|bool full path name if a custom script exists, false if no custom script exists
10055   */
10056  function custom_script_path() {
10057      global $CFG, $SCRIPT;
10058  
10059      if ($SCRIPT === null) {
10060          // Probably some weird external script.
10061          return false;
10062      }
10063  
10064      $scriptpath = $CFG->customscripts . $SCRIPT;
10065  
10066      // Check the custom script exists.
10067      if (file_exists($scriptpath) and is_file($scriptpath)) {
10068          return $scriptpath;
10069      } else {
10070          return false;
10071      }
10072  }
10073  
10074  /**
10075   * Returns whether or not the user object is a remote MNET user. This function
10076   * is in moodlelib because it does not rely on loading any of the MNET code.
10077   *
10078   * @param object $user A valid user object
10079   * @return bool        True if the user is from a remote Moodle.
10080   */
10081  function is_mnet_remote_user($user) {
10082      global $CFG;
10083  
10084      if (!isset($CFG->mnet_localhost_id)) {
10085          include_once($CFG->dirroot . '/mnet/lib.php');
10086          $env = new mnet_environment();
10087          $env->init();
10088          unset($env);
10089      }
10090  
10091      return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10092  }
10093  
10094  /**
10095   * This function will search for browser prefereed languages, setting Moodle
10096   * to use the best one available if $SESSION->lang is undefined
10097   */
10098  function setup_lang_from_browser() {
10099      global $CFG, $SESSION, $USER;
10100  
10101      if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10102          // Lang is defined in session or user profile, nothing to do.
10103          return;
10104      }
10105  
10106      if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10107          return;
10108      }
10109  
10110      // Extract and clean langs from headers.
10111      $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10112      $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10113      $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10114      $langs = array();
10115  
10116      $order = 1.0;
10117      foreach ($rawlangs as $lang) {
10118          if (strpos($lang, ';') === false) {
10119              $langs[(string)$order] = $lang;
10120              $order = $order-0.01;
10121          } else {
10122              $parts = explode(';', $lang);
10123              $pos = strpos($parts[1], '=');
10124              $langs[substr($parts[1], $pos+1)] = $parts[0];
10125          }
10126      }
10127      krsort($langs, SORT_NUMERIC);
10128  
10129      // Look for such langs under standard locations.
10130      foreach ($langs as $lang) {
10131          // Clean it properly for include.
10132          $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10133          if (get_string_manager()->translation_exists($lang, false)) {
10134              // Lang exists, set it in session.
10135              $SESSION->lang = $lang;
10136              // We have finished. Go out.
10137              break;
10138          }
10139      }
10140      return;
10141  }
10142  
10143  /**
10144   * Check if $url matches anything in proxybypass list
10145   *
10146   * Any errors just result in the proxy being used (least bad)
10147   *
10148   * @param string $url url to check
10149   * @return boolean true if we should bypass the proxy
10150   */
10151  function is_proxybypass( $url ) {
10152      global $CFG;
10153  
10154      // Sanity check.
10155      if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10156          return false;
10157      }
10158  
10159      // Get the host part out of the url.
10160      if (!$host = parse_url( $url, PHP_URL_HOST )) {
10161          return false;
10162      }
10163  
10164      // Get the possible bypass hosts into an array.
10165      $matches = explode( ',', $CFG->proxybypass );
10166  
10167      // Check for a exact match on the IP or in the domains.
10168      $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10169      $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10170  
10171      if ($isdomaininallowedlist || $isipinsubnetlist) {
10172          return true;
10173      }
10174  
10175      // Nothing matched.
10176      return false;
10177  }
10178  
10179  /**
10180   * Check if the passed navigation is of the new style
10181   *
10182   * @param mixed $navigation
10183   * @return bool true for yes false for no
10184   */
10185  function is_newnav($navigation) {
10186      if (is_array($navigation) && !empty($navigation['newnav'])) {
10187          return true;
10188      } else {
10189          return false;
10190      }
10191  }
10192  
10193  /**
10194   * Checks whether the given variable name is defined as a variable within the given object.
10195   *
10196   * This will NOT work with stdClass objects, which have no class variables.
10197   *
10198   * @param string $var The variable name
10199   * @param object $object The object to check
10200   * @return boolean
10201   */
10202  function in_object_vars($var, $object) {
10203      $classvars = get_class_vars(get_class($object));
10204      $classvars = array_keys($classvars);
10205      return in_array($var, $classvars);
10206  }
10207  
10208  /**
10209   * Returns an array without repeated objects.
10210   * This function is similar to array_unique, but for arrays that have objects as values
10211   *
10212   * @param array $array
10213   * @param bool $keepkeyassoc
10214   * @return array
10215   */
10216  function object_array_unique($array, $keepkeyassoc = true) {
10217      $duplicatekeys = array();
10218      $tmp         = array();
10219  
10220      foreach ($array as $key => $val) {
10221          // Convert objects to arrays, in_array() does not support objects.
10222          if (is_object($val)) {
10223              $val = (array)$val;
10224          }
10225  
10226          if (!in_array($val, $tmp)) {
10227              $tmp[] = $val;
10228          } else {
10229              $duplicatekeys[] = $key;
10230          }
10231      }
10232  
10233      foreach ($duplicatekeys as $key) {
10234          unset($array[$key]);
10235      }
10236  
10237      return $keepkeyassoc ? $array : array_values($array);
10238  }
10239  
10240  /**
10241   * Is a userid the primary administrator?
10242   *
10243   * @param int $userid int id of user to check
10244   * @return boolean
10245   */
10246  function is_primary_admin($userid) {
10247      $primaryadmin =  get_admin();
10248  
10249      if ($userid == $primaryadmin->id) {
10250          return true;
10251      } else {
10252          return false;
10253      }
10254  }
10255  
10256  /**
10257   * Returns the site identifier
10258   *
10259   * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10260   */
10261  function get_site_identifier() {
10262      global $CFG;
10263      // Check to see if it is missing. If so, initialise it.
10264      if (empty($CFG->siteidentifier)) {
10265          set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10266      }
10267      // Return it.
10268      return $CFG->siteidentifier;
10269  }
10270  
10271  /**
10272   * Check whether the given password has no more than the specified
10273   * number of consecutive identical characters.
10274   *
10275   * @param string $password   password to be checked against the password policy
10276   * @param integer $maxchars  maximum number of consecutive identical characters
10277   * @return bool
10278   */
10279  function check_consecutive_identical_characters($password, $maxchars) {
10280  
10281      if ($maxchars < 1) {
10282          return true; // Zero 0 is to disable this check.
10283      }
10284      if (strlen($password) <= $maxchars) {
10285          return true; // Too short to fail this test.
10286      }
10287  
10288      $previouschar = '';
10289      $consecutivecount = 1;
10290      foreach (str_split($password) as $char) {
10291          if ($char != $previouschar) {
10292              $consecutivecount = 1;
10293          } else {
10294              $consecutivecount++;
10295              if ($consecutivecount > $maxchars) {
10296                  return false; // Check failed already.
10297              }
10298          }
10299  
10300          $previouschar = $char;
10301      }
10302  
10303      return true;
10304  }
10305  
10306  /**
10307   * Helper function to do partial function binding.
10308   * so we can use it for preg_replace_callback, for example
10309   * this works with php functions, user functions, static methods and class methods
10310   * it returns you a callback that you can pass on like so:
10311   *
10312   * $callback = partial('somefunction', $arg1, $arg2);
10313   *     or
10314   * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10315   *     or even
10316   * $obj = new someclass();
10317   * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10318   *
10319   * and then the arguments that are passed through at calltime are appended to the argument list.
10320   *
10321   * @param mixed $function a php callback
10322   * @param mixed $arg1,... $argv arguments to partially bind with
10323   * @return array Array callback
10324   */
10325  function partial() {
10326      if (!class_exists('partial')) {
10327          /**
10328           * Used to manage function binding.
10329           * @copyright  2009 Penny Leach
10330           * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10331           */
10332          class partial{
10333              /** @var array */
10334              public $values = array();
10335              /** @var string The function to call as a callback. */
10336              public $func;
10337              /**
10338               * Constructor
10339               * @param string $func
10340               * @param array $args
10341               */
10342              public function __construct($func, $args) {
10343                  $this->values = $args;
10344                  $this->func = $func;
10345              }
10346              /**
10347               * Calls the callback function.
10348               * @return mixed
10349               */
10350              public function method() {
10351                  $args = func_get_args();
10352                  return call_user_func_array($this->func, array_merge($this->values, $args));
10353              }
10354          }
10355      }
10356      $args = func_get_args();
10357      $func = array_shift($args);
10358      $p = new partial($func, $args);
10359      return array($p, 'method');
10360  }
10361  
10362  /**
10363   * helper function to load up and initialise the mnet environment
10364   * this must be called before you use mnet functions.
10365   *
10366   * @return mnet_environment the equivalent of old $MNET global
10367   */
10368  function get_mnet_environment() {
10369      global $CFG;
10370      require_once($CFG->dirroot . '/mnet/lib.php');
10371      static $instance = null;
10372      if (empty($instance)) {
10373          $instance = new mnet_environment();
10374          $instance->init();
10375      }
10376      return $instance;
10377  }
10378  
10379  /**
10380   * during xmlrpc server code execution, any code wishing to access
10381   * information about the remote peer must use this to get it.
10382   *
10383   * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10384   */
10385  function get_mnet_remote_client() {
10386      if (!defined('MNET_SERVER')) {
10387          debugging(get_string('notinxmlrpcserver', 'mnet'));
10388          return false;
10389      }
10390      global $MNET_REMOTE_CLIENT;
10391      if (isset($MNET_REMOTE_CLIENT)) {
10392          return $MNET_REMOTE_CLIENT;
10393      }
10394      return false;
10395  }
10396  
10397  /**
10398   * during the xmlrpc server code execution, this will be called
10399   * to setup the object returned by {@link get_mnet_remote_client}
10400   *
10401   * @param mnet_remote_client $client the client to set up
10402   * @throws moodle_exception
10403   */
10404  function set_mnet_remote_client($client) {
10405      if (!defined('MNET_SERVER')) {
10406          throw new moodle_exception('notinxmlrpcserver', 'mnet');
10407      }
10408      global $MNET_REMOTE_CLIENT;
10409      $MNET_REMOTE_CLIENT = $client;
10410  }
10411  
10412  /**
10413   * return the jump url for a given remote user
10414   * this is used for rewriting forum post links in emails, etc
10415   *
10416   * @param stdclass $user the user to get the idp url for
10417   */
10418  function mnet_get_idp_jump_url($user) {
10419      global $CFG;
10420  
10421      static $mnetjumps = array();
10422      if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10423          $idp = mnet_get_peer_host($user->mnethostid);
10424          $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10425          $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10426      }
10427      return $mnetjumps[$user->mnethostid];
10428  }
10429  
10430  /**
10431   * Gets the homepage to use for the current user
10432   *
10433   * @return int One of HOMEPAGE_*
10434   */
10435  function get_home_page() {
10436      global $CFG;
10437  
10438      if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10439          // If dashboard is disabled, home will be set to default page.
10440          $defaultpage = get_default_home_page();
10441          if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10442              if (!empty($CFG->enabledashboard)) {
10443                  return HOMEPAGE_MY;
10444              } else {
10445                  return $defaultpage;
10446              }
10447          } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10448              return HOMEPAGE_MYCOURSES;
10449          } else {
10450              $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10451              if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10452                  // If the user was using the dashboard but it's disabled, return the default home page.
10453                  $userhomepage = $defaultpage;
10454              }
10455              return $userhomepage;
10456          }
10457      }
10458      return HOMEPAGE_SITE;
10459  }
10460  
10461  /**
10462   * Returns the default home page to display if current one is not defined or can't be applied.
10463   * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10464   *
10465   * @return int The default home page.
10466   */
10467  function get_default_home_page(): int {
10468      global $CFG;
10469  
10470      return !empty($CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10471  }
10472  
10473  /**
10474   * Gets the name of a course to be displayed when showing a list of courses.
10475   * By default this is just $course->fullname but user can configure it. The
10476   * result of this function should be passed through print_string.
10477   * @param stdClass|core_course_list_element $course Moodle course object
10478   * @return string Display name of course (either fullname or short + fullname)
10479   */
10480  function get_course_display_name_for_list($course) {
10481      global $CFG;
10482      if (!empty($CFG->courselistshortnames)) {
10483          if (!($course instanceof stdClass)) {
10484              $course = (object)convert_to_array($course);
10485          }
10486          return get_string('courseextendednamedisplay', '', $course);
10487      } else {
10488          return $course->fullname;
10489      }
10490  }
10491  
10492  /**
10493   * Safe analogue of unserialize() that can only parse arrays
10494   *
10495   * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10496   *
10497   * @param string $expression
10498   * @return array|bool either parsed array or false if parsing was impossible.
10499   */
10500  function unserialize_array($expression) {
10501  
10502      // Check the expression is an array.
10503      if (!preg_match('/^a:(\d+):/', $expression)) {
10504          return false;
10505      }
10506  
10507      $values = (array) unserialize_object($expression);
10508  
10509      // Callback that returns true if the given value is an unserialized object, executes recursively.
10510      $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10511          if (is_array($value)) {
10512              return (bool) array_filter($value, $invalidvaluecallback);
10513          }
10514          return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10515      };
10516  
10517      // Iterate over the result to ensure there are no stray objects.
10518      if (array_filter($values, $invalidvaluecallback)) {
10519          return false;
10520      }
10521  
10522      return $values;
10523  }
10524  
10525  /**
10526   * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10527   *
10528   * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10529   * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10530   * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10531   *
10532   * @param string $input
10533   * @return stdClass
10534   */
10535  function unserialize_object(string $input): stdClass {
10536      $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10537      return (object) $instance;
10538  }
10539  
10540  /**
10541   * The lang_string class
10542   *
10543   * This special class is used to create an object representation of a string request.
10544   * It is special because processing doesn't occur until the object is first used.
10545   * The class was created especially to aid performance in areas where strings were
10546   * required to be generated but were not necessarily used.
10547   * As an example the admin tree when generated uses over 1500 strings, of which
10548   * normally only 1/3 are ever actually printed at any time.
10549   * The performance advantage is achieved by not actually processing strings that
10550   * arn't being used, as such reducing the processing required for the page.
10551   *
10552   * How to use the lang_string class?
10553   *     There are two methods of using the lang_string class, first through the
10554   *     forth argument of the get_string function, and secondly directly.
10555   *     The following are examples of both.
10556   * 1. Through get_string calls e.g.
10557   *     $string = get_string($identifier, $component, $a, true);
10558   *     $string = get_string('yes', 'moodle', null, true);
10559   * 2. Direct instantiation
10560   *     $string = new lang_string($identifier, $component, $a, $lang);
10561   *     $string = new lang_string('yes');
10562   *
10563   * How do I use a lang_string object?
10564   *     The lang_string object makes use of a magic __toString method so that you
10565   *     are able to use the object exactly as you would use a string in most cases.
10566   *     This means you are able to collect it into a variable and then directly
10567   *     echo it, or concatenate it into another string, or similar.
10568   *     The other thing you can do is manually get the string by calling the
10569   *     lang_strings out method e.g.
10570   *         $string = new lang_string('yes');
10571   *         $string->out();
10572   *     Also worth noting is that the out method can take one argument, $lang which
10573   *     allows the developer to change the language on the fly.
10574   *
10575   * When should I use a lang_string object?
10576   *     The lang_string object is designed to be used in any situation where a
10577   *     string may not be needed, but needs to be generated.
10578   *     The admin tree is a good example of where lang_string objects should be
10579   *     used.
10580   *     A more practical example would be any class that requries strings that may
10581   *     not be printed (after all classes get renderer by renderers and who knows
10582   *     what they will do ;))
10583   *
10584   * When should I not use a lang_string object?
10585   *     Don't use lang_strings when you are going to use a string immediately.
10586   *     There is no need as it will be processed immediately and there will be no
10587   *     advantage, and in fact perhaps a negative hit as a class has to be
10588   *     instantiated for a lang_string object, however get_string won't require
10589   *     that.
10590   *
10591   * Limitations:
10592   * 1. You cannot use a lang_string object as an array offset. Doing so will
10593   *     result in PHP throwing an error. (You can use it as an object property!)
10594   *
10595   * @package    core
10596   * @category   string
10597   * @copyright  2011 Sam Hemelryk
10598   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10599   */
10600  class lang_string {
10601  
10602      /** @var string The strings identifier */
10603      protected $identifier;
10604      /** @var string The strings component. Default '' */
10605      protected $component = '';
10606      /** @var array|stdClass Any arguments required for the string. Default null */
10607      protected $a = null;
10608      /** @var string The language to use when processing the string. Default null */
10609      protected $lang = null;
10610  
10611      /** @var string The processed string (once processed) */
10612      protected $string = null;
10613  
10614      /**
10615       * A special boolean. If set to true then the object has been woken up and
10616       * cannot be regenerated. If this is set then $this->string MUST be used.
10617       * @var bool
10618       */
10619      protected $forcedstring = false;
10620  
10621      /**
10622       * Constructs a lang_string object
10623       *
10624       * This function should do as little processing as possible to ensure the best
10625       * performance for strings that won't be used.
10626       *
10627       * @param string $identifier The strings identifier
10628       * @param string $component The strings component
10629       * @param stdClass|array $a Any arguments the string requires
10630       * @param string $lang The language to use when processing the string.
10631       * @throws coding_exception
10632       */
10633      public function __construct($identifier, $component = '', $a = null, $lang = null) {
10634          if (empty($component)) {
10635              $component = 'moodle';
10636          }
10637  
10638          $this->identifier = $identifier;
10639          $this->component = $component;
10640          $this->lang = $lang;
10641  
10642          // We MUST duplicate $a to ensure that it if it changes by reference those
10643          // changes are not carried across.
10644          // To do this we always ensure $a or its properties/values are strings
10645          // and that any properties/values that arn't convertable are forgotten.
10646          if ($a !== null) {
10647              if (is_scalar($a)) {
10648                  $this->a = $a;
10649              } else if ($a instanceof lang_string) {
10650                  $this->a = $a->out();
10651              } else if (is_object($a) or is_array($a)) {
10652                  $a = (array)$a;
10653                  $this->a = array();
10654                  foreach ($a as $key => $value) {
10655                      // Make sure conversion errors don't get displayed (results in '').
10656                      if (is_array($value)) {
10657                          $this->a[$key] = '';
10658                      } else if (is_object($value)) {
10659                          if (method_exists($value, '__toString')) {
10660                              $this->a[$key] = $value->__toString();
10661                          } else {
10662                              $this->a[$key] = '';
10663                          }
10664                      } else {
10665                          $this->a[$key] = (string)$value;
10666                      }
10667                  }
10668              }
10669          }
10670  
10671          if (debugging(false, DEBUG_DEVELOPER)) {
10672              if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10673                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10674              }
10675              if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10676                  throw new coding_exception('Invalid string compontent. Please check your string definition');
10677              }
10678              if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10679                  debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10680              }
10681          }
10682      }
10683  
10684      /**
10685       * Processes the string.
10686       *
10687       * This function actually processes the string, stores it in the string property
10688       * and then returns it.
10689       * You will notice that this function is VERY similar to the get_string method.
10690       * That is because it is pretty much doing the same thing.
10691       * However as this function is an upgrade it isn't as tolerant to backwards
10692       * compatibility.
10693       *
10694       * @return string
10695       * @throws coding_exception
10696       */
10697      protected function get_string() {
10698          global $CFG;
10699  
10700          // Check if we need to process the string.
10701          if ($this->string === null) {
10702              // Check the quality of the identifier.
10703              if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10704                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER);
10705              }
10706  
10707              // Process the string.
10708              $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10709              // Debugging feature lets you display string identifier and component.
10710              if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10711                  $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10712              }
10713          }
10714          // Return the string.
10715          return $this->string;
10716      }
10717  
10718      /**
10719       * Returns the string
10720       *
10721       * @param string $lang The langauge to use when processing the string
10722       * @return string
10723       */
10724      public function out($lang = null) {
10725          if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10726              if ($this->forcedstring) {
10727                  debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10728                  return $this->get_string();
10729              }
10730              $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10731              return $translatedstring->out();
10732          }
10733          return $this->get_string();
10734      }
10735  
10736      /**
10737       * Magic __toString method for printing a string
10738       *
10739       * @return string
10740       */
10741      public function __toString() {
10742          return $this->get_string();
10743      }
10744  
10745      /**
10746       * Magic __set_state method used for var_export
10747       *
10748       * @param array $array
10749       * @return self
10750       */
10751      public static function __set_state(array $array): self {
10752          $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10753          $tmp->string = $array['string'];
10754          $tmp->forcedstring = $array['forcedstring'];
10755          return $tmp;
10756      }
10757  
10758      /**
10759       * Prepares the lang_string for sleep and stores only the forcedstring and
10760       * string properties... the string cannot be regenerated so we need to ensure
10761       * it is generated for this.
10762       *
10763       * @return string
10764       */
10765      public function __sleep() {
10766          $this->get_string();
10767          $this->forcedstring = true;
10768          return array('forcedstring', 'string', 'lang');
10769      }
10770  
10771      /**
10772       * Returns the identifier.
10773       *
10774       * @return string
10775       */
10776      public function get_identifier() {
10777          return $this->identifier;
10778      }
10779  
10780      /**
10781       * Returns the component.
10782       *
10783       * @return string
10784       */
10785      public function get_component() {
10786          return $this->component;
10787      }
10788  }
10789  
10790  /**
10791   * Get human readable name describing the given callable.
10792   *
10793   * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10794   * It does not check if the callable actually exists.
10795   *
10796   * @param callable|string|array $callable
10797   * @return string|bool Human readable name of callable, or false if not a valid callable.
10798   */
10799  function get_callable_name($callable) {
10800  
10801      if (!is_callable($callable, true, $name)) {
10802          return false;
10803  
10804      } else {
10805          return $name;
10806      }
10807  }
10808  
10809  /**
10810   * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10811   * Never put your faith on this function and rely on its accuracy as there might be false positives.
10812   * It just performs some simple checks, and mainly is used for places where we want to hide some options
10813   * such as site registration when $CFG->wwwroot is not publicly accessible.
10814   * Good thing is there is no false negative.
10815   * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10816   *
10817   * @return bool
10818   */
10819  function site_is_public() {
10820      global $CFG;
10821  
10822      // Return early if site admin has forced this setting.
10823      if (isset($CFG->site_is_public)) {
10824          return (bool)$CFG->site_is_public;
10825      }
10826  
10827      $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10828  
10829      if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10830          $ispublic = false;
10831      } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10832          $ispublic = false;
10833      } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10834          $ispublic = false;
10835      } else {
10836          $ispublic = true;
10837      }
10838  
10839      return $ispublic;
10840  }