Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
/lib/ -> moodlelib.php (source)

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 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  /**
 567   * Contact site support form/link disabled.
 568   */
 569  define('CONTACT_SUPPORT_DISABLED', 0);
 570  
 571  /**
 572   * Contact site support form/link only available to authenticated users.
 573   */
 574  define('CONTACT_SUPPORT_AUTHENTICATED', 1);
 575  
 576  /**
 577   * Contact site support form/link available to anyone visiting the site.
 578   */
 579  define('CONTACT_SUPPORT_ANYONE', 2);
 580  
 581  // PARAMETER HANDLING.
 582  
 583  /**
 584   * Returns a particular value for the named variable, taken from
 585   * POST or GET.  If the parameter doesn't exist then an error is
 586   * thrown because we require this variable.
 587   *
 588   * This function should be used to initialise all required values
 589   * in a script that are based on parameters.  Usually it will be
 590   * used like this:
 591   *    $id = required_param('id', PARAM_INT);
 592   *
 593   * Please note the $type parameter is now required and the value can not be array.
 594   *
 595   * @param string $parname the name of the page parameter we want
 596   * @param string $type expected type of parameter
 597   * @return mixed
 598   * @throws coding_exception
 599   */
 600  function required_param($parname, $type) {
 601      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 602          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
 603      }
 604      // POST has precedence.
 605      if (isset($_POST[$parname])) {
 606          $param = $_POST[$parname];
 607      } else if (isset($_GET[$parname])) {
 608          $param = $_GET[$parname];
 609      } else {
 610          throw new \moodle_exception('missingparam', '', '', $parname);
 611      }
 612  
 613      if (is_array($param)) {
 614          debugging('Invalid array parameter detected in required_param(): '.$parname);
 615          // TODO: switch to fatal error in Moodle 2.3.
 616          return required_param_array($parname, $type);
 617      }
 618  
 619      return clean_param($param, $type);
 620  }
 621  
 622  /**
 623   * Returns a particular array value for the named variable, taken from
 624   * POST or GET.  If the parameter doesn't exist then an error is
 625   * thrown because we require this variable.
 626   *
 627   * This function should be used to initialise all required values
 628   * in a script that are based on parameters.  Usually it will be
 629   * used like this:
 630   *    $ids = required_param_array('ids', PARAM_INT);
 631   *
 632   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 633   *
 634   * @param string $parname the name of the page parameter we want
 635   * @param string $type expected type of parameter
 636   * @return array
 637   * @throws coding_exception
 638   */
 639  function required_param_array($parname, $type) {
 640      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 641          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
 642      }
 643      // POST has precedence.
 644      if (isset($_POST[$parname])) {
 645          $param = $_POST[$parname];
 646      } else if (isset($_GET[$parname])) {
 647          $param = $_GET[$parname];
 648      } else {
 649          throw new \moodle_exception('missingparam', '', '', $parname);
 650      }
 651      if (!is_array($param)) {
 652          throw new \moodle_exception('missingparam', '', '', $parname);
 653      }
 654  
 655      $result = array();
 656      foreach ($param as $key => $value) {
 657          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 658              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
 659              continue;
 660          }
 661          $result[$key] = clean_param($value, $type);
 662      }
 663  
 664      return $result;
 665  }
 666  
 667  /**
 668   * Returns a particular value for the named variable, taken from
 669   * POST or GET, otherwise returning a given default.
 670   *
 671   * This function should be used to initialise all optional values
 672   * in a script that are based on parameters.  Usually it will be
 673   * used like this:
 674   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
 675   *
 676   * Please note the $type parameter is now required and the value can not be array.
 677   *
 678   * @param string $parname the name of the page parameter we want
 679   * @param mixed  $default the default value to return if nothing is found
 680   * @param string $type expected type of parameter
 681   * @return mixed
 682   * @throws coding_exception
 683   */
 684  function optional_param($parname, $default, $type) {
 685      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 686          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 687      }
 688  
 689      // POST has precedence.
 690      if (isset($_POST[$parname])) {
 691          $param = $_POST[$parname];
 692      } else if (isset($_GET[$parname])) {
 693          $param = $_GET[$parname];
 694      } else {
 695          return $default;
 696      }
 697  
 698      if (is_array($param)) {
 699          debugging('Invalid array parameter detected in required_param(): '.$parname);
 700          // TODO: switch to $default in Moodle 2.3.
 701          return optional_param_array($parname, $default, $type);
 702      }
 703  
 704      return clean_param($param, $type);
 705  }
 706  
 707  /**
 708   * Returns a particular array value for the named variable, taken from
 709   * POST or GET, otherwise returning a given default.
 710   *
 711   * This function should be used to initialise all optional values
 712   * in a script that are based on parameters.  Usually it will be
 713   * used like this:
 714   *    $ids = optional_param('id', array(), PARAM_INT);
 715   *
 716   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 717   *
 718   * @param string $parname the name of the page parameter we want
 719   * @param mixed $default the default value to return if nothing is found
 720   * @param string $type expected type of parameter
 721   * @return array
 722   * @throws coding_exception
 723   */
 724  function optional_param_array($parname, $default, $type) {
 725      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 726          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 727      }
 728  
 729      // POST has precedence.
 730      if (isset($_POST[$parname])) {
 731          $param = $_POST[$parname];
 732      } else if (isset($_GET[$parname])) {
 733          $param = $_GET[$parname];
 734      } else {
 735          return $default;
 736      }
 737      if (!is_array($param)) {
 738          debugging('optional_param_array() expects array parameters only: '.$parname);
 739          return $default;
 740      }
 741  
 742      $result = array();
 743      foreach ($param as $key => $value) {
 744          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 745              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
 746              continue;
 747          }
 748          $result[$key] = clean_param($value, $type);
 749      }
 750  
 751      return $result;
 752  }
 753  
 754  /**
 755   * Strict validation of parameter values, the values are only converted
 756   * to requested PHP type. Internally it is using clean_param, the values
 757   * before and after cleaning must be equal - otherwise
 758   * an invalid_parameter_exception is thrown.
 759   * Objects and classes are not accepted.
 760   *
 761   * @param mixed $param
 762   * @param string $type PARAM_ constant
 763   * @param bool $allownull are nulls valid value?
 764   * @param string $debuginfo optional debug information
 765   * @return mixed the $param value converted to PHP type
 766   * @throws invalid_parameter_exception if $param is not of given type
 767   */
 768  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
 769      if (is_null($param)) {
 770          if ($allownull == NULL_ALLOWED) {
 771              return null;
 772          } else {
 773              throw new invalid_parameter_exception($debuginfo);
 774          }
 775      }
 776      if (is_array($param) or is_object($param)) {
 777          throw new invalid_parameter_exception($debuginfo);
 778      }
 779  
 780      $cleaned = clean_param($param, $type);
 781  
 782      if ($type == PARAM_FLOAT) {
 783          // Do not detect precision loss here.
 784          if (is_float($param) or is_int($param)) {
 785              // These always fit.
 786          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
 787              throw new invalid_parameter_exception($debuginfo);
 788          }
 789      } else if ((string)$param !== (string)$cleaned) {
 790          // Conversion to string is usually lossless.
 791          throw new invalid_parameter_exception($debuginfo);
 792      }
 793  
 794      return $cleaned;
 795  }
 796  
 797  /**
 798   * Makes sure array contains only the allowed types, this function does not validate array key names!
 799   *
 800   * <code>
 801   * $options = clean_param($options, PARAM_INT);
 802   * </code>
 803   *
 804   * @param array|null $param the variable array we are cleaning
 805   * @param string $type expected format of param after cleaning.
 806   * @param bool $recursive clean recursive arrays
 807   * @return array
 808   * @throws coding_exception
 809   */
 810  function clean_param_array(?array $param, $type, $recursive = false) {
 811      // Convert null to empty array.
 812      $param = (array)$param;
 813      foreach ($param as $key => $value) {
 814          if (is_array($value)) {
 815              if ($recursive) {
 816                  $param[$key] = clean_param_array($value, $type, true);
 817              } else {
 818                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
 819              }
 820          } else {
 821              $param[$key] = clean_param($value, $type);
 822          }
 823      }
 824      return $param;
 825  }
 826  
 827  /**
 828   * Used by {@link optional_param()} and {@link required_param()} to
 829   * clean the variables and/or cast to specific types, based on
 830   * an options field.
 831   * <code>
 832   * $course->format = clean_param($course->format, PARAM_ALPHA);
 833   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
 834   * </code>
 835   *
 836   * @param mixed $param the variable we are cleaning
 837   * @param string $type expected format of param after cleaning.
 838   * @return mixed
 839   * @throws coding_exception
 840   */
 841  function clean_param($param, $type) {
 842      global $CFG;
 843  
 844      if (is_array($param)) {
 845          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
 846      } else if (is_object($param)) {
 847          if (method_exists($param, '__toString')) {
 848              $param = $param->__toString();
 849          } else {
 850              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
 851          }
 852      }
 853  
 854      switch ($type) {
 855          case PARAM_RAW:
 856              // No cleaning at all.
 857              $param = fix_utf8($param);
 858              return $param;
 859  
 860          case PARAM_RAW_TRIMMED:
 861              // No cleaning, but strip leading and trailing whitespace.
 862              $param = (string)fix_utf8($param);
 863              return trim($param);
 864  
 865          case PARAM_CLEAN:
 866              // General HTML cleaning, try to use more specific type if possible this is deprecated!
 867              // Please use more specific type instead.
 868              if (is_numeric($param)) {
 869                  return $param;
 870              }
 871              $param = fix_utf8($param);
 872              // Sweep for scripts, etc.
 873              return clean_text($param);
 874  
 875          case PARAM_CLEANHTML:
 876              // Clean html fragment.
 877              $param = (string)fix_utf8($param);
 878              // Sweep for scripts, etc.
 879              $param = clean_text($param, FORMAT_HTML);
 880              return trim($param);
 881  
 882          case PARAM_INT:
 883              // Convert to integer.
 884              return (int)$param;
 885  
 886          case PARAM_FLOAT:
 887              // Convert to float.
 888              return (float)$param;
 889  
 890          case PARAM_LOCALISEDFLOAT:
 891              // Convert to float.
 892              return unformat_float($param, true);
 893  
 894          case PARAM_ALPHA:
 895              // Remove everything not `a-z`.
 896              return preg_replace('/[^a-zA-Z]/i', '', (string)$param);
 897  
 898          case PARAM_ALPHAEXT:
 899              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
 900              return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param);
 901  
 902          case PARAM_ALPHANUM:
 903              // Remove everything not `a-zA-Z0-9`.
 904              return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param);
 905  
 906          case PARAM_ALPHANUMEXT:
 907              // Remove everything not `a-zA-Z0-9_-`.
 908              return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param);
 909  
 910          case PARAM_SEQUENCE:
 911              // Remove everything not `0-9,`.
 912              return preg_replace('/[^0-9,]/i', '', (string)$param);
 913  
 914          case PARAM_BOOL:
 915              // Convert to 1 or 0.
 916              $tempstr = strtolower((string)$param);
 917              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
 918                  $param = 1;
 919              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
 920                  $param = 0;
 921              } else {
 922                  $param = empty($param) ? 0 : 1;
 923              }
 924              return $param;
 925  
 926          case PARAM_NOTAGS:
 927              // Strip all tags.
 928              $param = fix_utf8($param);
 929              return strip_tags((string)$param);
 930  
 931          case PARAM_TEXT:
 932              // Leave only tags needed for multilang.
 933              $param = fix_utf8($param);
 934              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
 935              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
 936              do {
 937                  if (strpos((string)$param, '</lang>') !== false) {
 938                      // Old and future mutilang syntax.
 939                      $param = strip_tags($param, '<lang>');
 940                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 941                          break;
 942                      }
 943                      $open = false;
 944                      foreach ($matches[0] as $match) {
 945                          if ($match === '</lang>') {
 946                              if ($open) {
 947                                  $open = false;
 948                                  continue;
 949                              } else {
 950                                  break 2;
 951                              }
 952                          }
 953                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
 954                              break 2;
 955                          } else {
 956                              $open = true;
 957                          }
 958                      }
 959                      if ($open) {
 960                          break;
 961                      }
 962                      return $param;
 963  
 964                  } else if (strpos((string)$param, '</span>') !== false) {
 965                      // Current problematic multilang syntax.
 966                      $param = strip_tags($param, '<span>');
 967                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 968                          break;
 969                      }
 970                      $open = false;
 971                      foreach ($matches[0] as $match) {
 972                          if ($match === '</span>') {
 973                              if ($open) {
 974                                  $open = false;
 975                                  continue;
 976                              } else {
 977                                  break 2;
 978                              }
 979                          }
 980                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
 981                              break 2;
 982                          } else {
 983                              $open = true;
 984                          }
 985                      }
 986                      if ($open) {
 987                          break;
 988                      }
 989                      return $param;
 990                  }
 991              } while (false);
 992              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
 993              return strip_tags((string)$param);
 994  
 995          case PARAM_COMPONENT:
 996              // We do not want any guessing here, either the name is correct or not
 997              // please note only normalised component names are accepted.
 998              $param = (string)$param;
 999              if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
1000                  return '';
1001              }
1002              if (strpos($param, '__') !== false) {
1003                  return '';
1004              }
1005              if (strpos($param, 'mod_') === 0) {
1006                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
1007                  if (substr_count($param, '_') != 1) {
1008                      return '';
1009                  }
1010              }
1011              return $param;
1012  
1013          case PARAM_PLUGIN:
1014          case PARAM_AREA:
1015              // We do not want any guessing here, either the name is correct or not.
1016              if (!is_valid_plugin_name($param)) {
1017                  return '';
1018              }
1019              return $param;
1020  
1021          case PARAM_SAFEDIR:
1022              // Remove everything not a-zA-Z0-9_- .
1023              return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param);
1024  
1025          case PARAM_SAFEPATH:
1026              // Remove everything not a-zA-Z0-9/_- .
1027              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param);
1028  
1029          case PARAM_FILE:
1030              // Strip all suspicious characters from filename.
1031              $param = (string)fix_utf8($param);
1032              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
1033              if ($param === '.' || $param === '..') {
1034                  $param = '';
1035              }
1036              return $param;
1037  
1038          case PARAM_PATH:
1039              // Strip all suspicious characters from file path.
1040              $param = (string)fix_utf8($param);
1041              $param = str_replace('\\', '/', $param);
1042  
1043              // Explode the path and clean each element using the PARAM_FILE rules.
1044              $breadcrumb = explode('/', $param);
1045              foreach ($breadcrumb as $key => $crumb) {
1046                  if ($crumb === '.' && $key === 0) {
1047                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
1048                  } else {
1049                      $crumb = clean_param($crumb, PARAM_FILE);
1050                  }
1051                  $breadcrumb[$key] = $crumb;
1052              }
1053              $param = implode('/', $breadcrumb);
1054  
1055              // Remove multiple current path (./././) and multiple slashes (///).
1056              $param = preg_replace('~//+~', '/', $param);
1057              $param = preg_replace('~/(\./)+~', '/', $param);
1058              return $param;
1059  
1060          case PARAM_HOST:
1061              // Allow FQDN or IPv4 dotted quad.
1062              $param = preg_replace('/[^\.\d\w-]/', '', (string)$param );
1063              // Match ipv4 dotted quad.
1064              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1065                  // Confirm values are ok.
1066                  if ( $match[0] > 255
1067                       || $match[1] > 255
1068                       || $match[3] > 255
1069                       || $match[4] > 255 ) {
1070                      // Hmmm, what kind of dotted quad is this?
1071                      $param = '';
1072                  }
1073              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1074                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1075                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1076                         ) {
1077                  // All is ok - $param is respected.
1078              } else {
1079                  // All is not ok...
1080                  $param='';
1081              }
1082              return $param;
1083  
1084          case PARAM_URL:
1085              // Allow safe urls.
1086              $param = (string)fix_utf8($param);
1087              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1088              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1089                  // All is ok, param is respected.
1090              } else {
1091                  // Not really ok.
1092                  $param ='';
1093              }
1094              return $param;
1095  
1096          case PARAM_LOCALURL:
1097              // Allow http absolute, root relative and relative URLs within wwwroot.
1098              $param = clean_param($param, PARAM_URL);
1099              if (!empty($param)) {
1100  
1101                  if ($param === $CFG->wwwroot) {
1102                      // Exact match;
1103                  } else if (preg_match(':^/:', $param)) {
1104                      // Root-relative, ok!
1105                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1106                      // Absolute, and matches our wwwroot.
1107                  } else {
1108  
1109                      // Relative - let's make sure there are no tricks.
1110                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
1111                          // Looks ok.
1112                      } else {
1113                          $param = '';
1114                      }
1115                  }
1116              }
1117              return $param;
1118  
1119          case PARAM_PEM:
1120              $param = trim((string)$param);
1121              // PEM formatted strings may contain letters/numbers and the symbols:
1122              //   forward slash: /
1123              //   plus sign:     +
1124              //   equal sign:    =
1125              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1126              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1127                  list($wholething, $body) = $matches;
1128                  unset($wholething, $matches);
1129                  $b64 = clean_param($body, PARAM_BASE64);
1130                  if (!empty($b64)) {
1131                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1132                  } else {
1133                      return '';
1134                  }
1135              }
1136              return '';
1137  
1138          case PARAM_BASE64:
1139              if (!empty($param)) {
1140                  // PEM formatted strings may contain letters/numbers and the symbols
1141                  //   forward slash: /
1142                  //   plus sign:     +
1143                  //   equal sign:    =.
1144                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1145                      return '';
1146                  }
1147                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1148                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1149                  // than (or equal to) 64 characters long.
1150                  for ($i=0, $j=count($lines); $i < $j; $i++) {
1151                      if ($i + 1 == $j) {
1152                          if (64 < strlen($lines[$i])) {
1153                              return '';
1154                          }
1155                          continue;
1156                      }
1157  
1158                      if (64 != strlen($lines[$i])) {
1159                          return '';
1160                      }
1161                  }
1162                  return implode("\n", $lines);
1163              } else {
1164                  return '';
1165              }
1166  
1167          case PARAM_TAG:
1168              $param = (string)fix_utf8($param);
1169              // Please note it is not safe to use the tag name directly anywhere,
1170              // it must be processed with s(), urlencode() before embedding anywhere.
1171              // Remove some nasties.
1172              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1173              // Convert many whitespace chars into one.
1174              $param = preg_replace('/\s+/u', ' ', $param);
1175              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1176              return $param;
1177  
1178          case PARAM_TAGLIST:
1179              $param = (string)fix_utf8($param);
1180              $tags = explode(',', $param);
1181              $result = array();
1182              foreach ($tags as $tag) {
1183                  $res = clean_param($tag, PARAM_TAG);
1184                  if ($res !== '') {
1185                      $result[] = $res;
1186                  }
1187              }
1188              if ($result) {
1189                  return implode(',', $result);
1190              } else {
1191                  return '';
1192              }
1193  
1194          case PARAM_CAPABILITY:
1195              if (get_capability_info($param)) {
1196                  return $param;
1197              } else {
1198                  return '';
1199              }
1200  
1201          case PARAM_PERMISSION:
1202              $param = (int)$param;
1203              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1204                  return $param;
1205              } else {
1206                  return CAP_INHERIT;
1207              }
1208  
1209          case PARAM_AUTH:
1210              $param = clean_param($param, PARAM_PLUGIN);
1211              if (empty($param)) {
1212                  return '';
1213              } else if (exists_auth_plugin($param)) {
1214                  return $param;
1215              } else {
1216                  return '';
1217              }
1218  
1219          case PARAM_LANG:
1220              $param = clean_param($param, PARAM_SAFEDIR);
1221              if (get_string_manager()->translation_exists($param)) {
1222                  return $param;
1223              } else {
1224                  // Specified language is not installed or param malformed.
1225                  return '';
1226              }
1227  
1228          case PARAM_THEME:
1229              $param = clean_param($param, PARAM_PLUGIN);
1230              if (empty($param)) {
1231                  return '';
1232              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1233                  return $param;
1234              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1235                  return $param;
1236              } else {
1237                  // Specified theme is not installed.
1238                  return '';
1239              }
1240  
1241          case PARAM_USERNAME:
1242              $param = (string)fix_utf8($param);
1243              $param = trim($param);
1244              // Convert uppercase to lowercase MDL-16919.
1245              $param = core_text::strtolower($param);
1246              if (empty($CFG->extendedusernamechars)) {
1247                  $param = str_replace(" " , "", $param);
1248                  // Regular expression, eliminate all chars EXCEPT:
1249                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1250                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1251              }
1252              return $param;
1253  
1254          case PARAM_EMAIL:
1255              $param = fix_utf8($param);
1256              if (validate_email($param ?? '')) {
1257                  return $param;
1258              } else {
1259                  return '';
1260              }
1261  
1262          case PARAM_STRINGID:
1263              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) {
1264                  return $param;
1265              } else {
1266                  return '';
1267              }
1268  
1269          case PARAM_TIMEZONE:
1270              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1271              $param = (string)fix_utf8($param);
1272              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1273              if (preg_match($timezonepattern, $param)) {
1274                  return $param;
1275              } else {
1276                  return '';
1277              }
1278  
1279          default:
1280              // Doh! throw error, switched parameters in optional_param or another serious problem.
1281              throw new \moodle_exception("unknownparamtype", '', '', $type);
1282      }
1283  }
1284  
1285  /**
1286   * Whether the PARAM_* type is compatible in RTL.
1287   *
1288   * Being compatible with RTL means that the data they contain can flow
1289   * from right-to-left or left-to-right without compromising the user experience.
1290   *
1291   * Take URLs for example, they are not RTL compatible as they should always
1292   * flow from the left to the right. This also applies to numbers, email addresses,
1293   * configuration snippets, base64 strings, etc...
1294   *
1295   * This function tries to best guess which parameters can contain localised strings.
1296   *
1297   * @param string $paramtype Constant PARAM_*.
1298   * @return bool
1299   */
1300  function is_rtl_compatible($paramtype) {
1301      return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1302  }
1303  
1304  /**
1305   * Makes sure the data is using valid utf8, invalid characters are discarded.
1306   *
1307   * Note: this function is not intended for full objects with methods and private properties.
1308   *
1309   * @param mixed $value
1310   * @return mixed with proper utf-8 encoding
1311   */
1312  function fix_utf8($value) {
1313      if (is_null($value) or $value === '') {
1314          return $value;
1315  
1316      } else if (is_string($value)) {
1317          if ((string)(int)$value === $value) {
1318              // Shortcut.
1319              return $value;
1320          }
1321  
1322          // Remove null bytes or invalid Unicode sequences from value.
1323          $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
1324  
1325          // Note: this duplicates min_fix_utf8() intentionally.
1326          static $buggyiconv = null;
1327          if ($buggyiconv === null) {
1328              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1329          }
1330  
1331          if ($buggyiconv) {
1332              if (function_exists('mb_convert_encoding')) {
1333                  $subst = mb_substitute_character();
1334                  mb_substitute_character('none');
1335                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1336                  mb_substitute_character($subst);
1337  
1338              } else {
1339                  // Warn admins on admin/index.php page.
1340                  $result = $value;
1341              }
1342  
1343          } else {
1344              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1345          }
1346  
1347          return $result;
1348  
1349      } else if (is_array($value)) {
1350          foreach ($value as $k => $v) {
1351              $value[$k] = fix_utf8($v);
1352          }
1353          return $value;
1354  
1355      } else if (is_object($value)) {
1356          // Do not modify original.
1357          $value = clone($value);
1358          foreach ($value as $k => $v) {
1359              $value->$k = fix_utf8($v);
1360          }
1361          return $value;
1362  
1363      } else {
1364          // This is some other type, no utf-8 here.
1365          return $value;
1366      }
1367  }
1368  
1369  /**
1370   * Return true if given value is integer or string with integer value
1371   *
1372   * @param mixed $value String or Int
1373   * @return bool true if number, false if not
1374   */
1375  function is_number($value) {
1376      if (is_int($value)) {
1377          return true;
1378      } else if (is_string($value)) {
1379          return ((string)(int)$value) === $value;
1380      } else {
1381          return false;
1382      }
1383  }
1384  
1385  /**
1386   * Returns host part from url.
1387   *
1388   * @param string $url full url
1389   * @return string host, null if not found
1390   */
1391  function get_host_from_url($url) {
1392      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1393      if ($matches) {
1394          return $matches[1];
1395      }
1396      return null;
1397  }
1398  
1399  /**
1400   * Tests whether anything was returned by text editor
1401   *
1402   * This function is useful for testing whether something you got back from
1403   * the HTML editor actually contains anything. Sometimes the HTML editor
1404   * appear to be empty, but actually you get back a <br> tag or something.
1405   *
1406   * @param string $string a string containing HTML.
1407   * @return boolean does the string contain any actual content - that is text,
1408   * images, objects, etc.
1409   */
1410  function html_is_blank($string) {
1411      return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
1412  }
1413  
1414  /**
1415   * Set a key in global configuration
1416   *
1417   * Set a key/value pair in both this session's {@link $CFG} global variable
1418   * and in the 'config' database table for future sessions.
1419   *
1420   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1421   * In that case it doesn't affect $CFG.
1422   *
1423   * A NULL value will delete the entry.
1424   *
1425   * NOTE: this function is called from lib/db/upgrade.php
1426   *
1427   * @param string $name the key to set
1428   * @param string $value the value to set (without magic quotes)
1429   * @param string $plugin (optional) the plugin scope, default null
1430   * @return bool true or exception
1431   */
1432  function set_config($name, $value, $plugin = null) {
1433      global $CFG, $DB;
1434  
1435      // Redirect to appropriate handler when value is null.
1436      if ($value === null) {
1437          return unset_config($name, $plugin);
1438      }
1439  
1440      // Set variables determining conditions and where to store the new config.
1441      // Plugin config goes to {config_plugins}, core config goes to {config}.
1442      $iscore = empty($plugin);
1443      if ($iscore) {
1444          // If it's for core config.
1445          $table = 'config';
1446          $conditions = ['name' => $name];
1447          $invalidatecachekey = 'core';
1448      } else {
1449          // If it's a plugin.
1450          $table = 'config_plugins';
1451          $conditions = ['name' => $name, 'plugin' => $plugin];
1452          $invalidatecachekey = $plugin;
1453      }
1454  
1455      // DB handling - checks for existing config, updating or inserting only if necessary.
1456      $invalidatecache = true;
1457      $inserted = false;
1458      $record = $DB->get_record($table, $conditions, 'id, value');
1459      if ($record === false) {
1460          // Inserts a new config record.
1461          $config = new stdClass();
1462          $config->name  = $name;
1463          $config->value = $value;
1464          if (!$iscore) {
1465              $config->plugin = $plugin;
1466          }
1467          $inserted = $DB->insert_record($table, $config, false);
1468      } else if ($invalidatecache = ($record->value !== $value)) {
1469          // Record exists - Check and only set new value if it has changed.
1470          $DB->set_field($table, 'value', $value, ['id' => $record->id]);
1471      }
1472  
1473      if ($iscore && !isset($CFG->config_php_settings[$name])) {
1474          // So it's defined for this invocation at least.
1475          // Settings from db are always strings.
1476          $CFG->$name = (string) $value;
1477      }
1478  
1479      // When setting config during a Behat test (in the CLI script, not in the web browser
1480      // requests), remember which ones are set so that we can clear them later.
1481      if ($iscore && $inserted && defined('BEHAT_TEST')) {
1482          $CFG->behat_cli_added_config[$name] = true;
1483      }
1484  
1485      // Update siteidentifier cache, if required.
1486      if ($iscore && $name === 'siteidentifier') {
1487          cache_helper::update_site_identifier($value);
1488      }
1489  
1490      // Invalidate cache, if required.
1491      if ($invalidatecache) {
1492          cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
1493      }
1494  
1495      return true;
1496  }
1497  
1498  /**
1499   * Get configuration values from the global config table
1500   * or the config_plugins table.
1501   *
1502   * If called with one parameter, it will load all the config
1503   * variables for one plugin, and return them as an object.
1504   *
1505   * If called with 2 parameters it will return a string single
1506   * value or false if the value is not found.
1507   *
1508   * NOTE: this function is called from lib/db/upgrade.php
1509   *
1510   * @param string $plugin full component name
1511   * @param string $name default null
1512   * @return mixed hash-like object or single value, return false no config found
1513   * @throws dml_exception
1514   */
1515  function get_config($plugin, $name = null) {
1516      global $CFG, $DB;
1517  
1518      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1519          $forced =& $CFG->config_php_settings;
1520          $iscore = true;
1521          $plugin = 'core';
1522      } else {
1523          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1524              $forced =& $CFG->forced_plugin_settings[$plugin];
1525          } else {
1526              $forced = array();
1527          }
1528          $iscore = false;
1529      }
1530  
1531      if (!isset($CFG->siteidentifier)) {
1532          try {
1533              // This may throw an exception during installation, which is how we detect the
1534              // need to install the database. For more details see {@see initialise_cfg()}.
1535              $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1536          } catch (dml_exception $ex) {
1537              // Set siteidentifier to false. We don't want to trip this continually.
1538              $siteidentifier = false;
1539              throw $ex;
1540          }
1541      }
1542  
1543      if (!empty($name)) {
1544          if (array_key_exists($name, $forced)) {
1545              return (string)$forced[$name];
1546          } else if ($name === 'siteidentifier' && $plugin == 'core') {
1547              return $CFG->siteidentifier;
1548          }
1549      }
1550  
1551      $cache = cache::make('core', 'config');
1552      $result = $cache->get($plugin);
1553      if ($result === false) {
1554          // The user is after a recordset.
1555          if (!$iscore) {
1556              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1557          } else {
1558              // This part is not really used any more, but anyway...
1559              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1560          }
1561          $cache->set($plugin, $result);
1562      }
1563  
1564      if (!empty($name)) {
1565          if (array_key_exists($name, $result)) {
1566              return $result[$name];
1567          }
1568          return false;
1569      }
1570  
1571      if ($plugin === 'core') {
1572          $result['siteidentifier'] = $CFG->siteidentifier;
1573      }
1574  
1575      foreach ($forced as $key => $value) {
1576          if (is_null($value) or is_array($value) or is_object($value)) {
1577              // We do not want any extra mess here, just real settings that could be saved in db.
1578              unset($result[$key]);
1579          } else {
1580              // Convert to string as if it went through the DB.
1581              $result[$key] = (string)$value;
1582          }
1583      }
1584  
1585      return (object)$result;
1586  }
1587  
1588  /**
1589   * Removes a key from global configuration.
1590   *
1591   * NOTE: this function is called from lib/db/upgrade.php
1592   *
1593   * @param string $name the key to set
1594   * @param string $plugin (optional) the plugin scope
1595   * @return boolean whether the operation succeeded.
1596   */
1597  function unset_config($name, $plugin=null) {
1598      global $CFG, $DB;
1599  
1600      if (empty($plugin)) {
1601          unset($CFG->$name);
1602          $DB->delete_records('config', array('name' => $name));
1603          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1604      } else {
1605          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1606          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1607      }
1608  
1609      return true;
1610  }
1611  
1612  /**
1613   * Remove all the config variables for a given plugin.
1614   *
1615   * NOTE: this function is called from lib/db/upgrade.php
1616   *
1617   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1618   * @return boolean whether the operation succeeded.
1619   */
1620  function unset_all_config_for_plugin($plugin) {
1621      global $DB;
1622      // Delete from the obvious config_plugins first.
1623      $DB->delete_records('config_plugins', array('plugin' => $plugin));
1624      // Next delete any suspect settings from config.
1625      $like = $DB->sql_like('name', '?', true, true, false, '|');
1626      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1627      $DB->delete_records_select('config', $like, $params);
1628      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1629      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1630  
1631      return true;
1632  }
1633  
1634  /**
1635   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1636   *
1637   * All users are verified if they still have the necessary capability.
1638   *
1639   * @param string $value the value of the config setting.
1640   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1641   * @param bool $includeadmins include administrators.
1642   * @return array of user objects.
1643   */
1644  function get_users_from_config($value, $capability, $includeadmins = true) {
1645      if (empty($value) or $value === '$@NONE@$') {
1646          return array();
1647      }
1648  
1649      // We have to make sure that users still have the necessary capability,
1650      // it should be faster to fetch them all first and then test if they are present
1651      // instead of validating them one-by-one.
1652      $users = get_users_by_capability(context_system::instance(), $capability);
1653      if ($includeadmins) {
1654          $admins = get_admins();
1655          foreach ($admins as $admin) {
1656              $users[$admin->id] = $admin;
1657          }
1658      }
1659  
1660      if ($value === '$@ALL@$') {
1661          return $users;
1662      }
1663  
1664      $result = array(); // Result in correct order.
1665      $allowed = explode(',', $value);
1666      foreach ($allowed as $uid) {
1667          if (isset($users[$uid])) {
1668              $user = $users[$uid];
1669              $result[$user->id] = $user;
1670          }
1671      }
1672  
1673      return $result;
1674  }
1675  
1676  
1677  /**
1678   * Invalidates browser caches and cached data in temp.
1679   *
1680   * @return void
1681   */
1682  function purge_all_caches() {
1683      purge_caches();
1684  }
1685  
1686  /**
1687   * Selectively invalidate different types of cache.
1688   *
1689   * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1690   * areas alone or in combination.
1691   *
1692   * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1693   *        'muc'    Purge MUC caches?
1694   *        'theme'  Purge theme cache?
1695   *        'lang'   Purge language string cache?
1696   *        'js'     Purge javascript cache?
1697   *        'filter' Purge text filter cache?
1698   *        'other'  Purge all other caches?
1699   */
1700  function purge_caches($options = []) {
1701      $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1702      if (empty(array_filter($options))) {
1703          $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1704      } else {
1705          $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1706      }
1707      if ($options['muc']) {
1708          cache_helper::purge_all();
1709      }
1710      if ($options['theme']) {
1711          theme_reset_all_caches();
1712      }
1713      if ($options['lang']) {
1714          get_string_manager()->reset_caches();
1715      }
1716      if ($options['js']) {
1717          js_reset_all_caches();
1718      }
1719      if ($options['template']) {
1720          template_reset_all_caches();
1721      }
1722      if ($options['filter']) {
1723          reset_text_filters_cache();
1724      }
1725      if ($options['other']) {
1726          purge_other_caches();
1727      }
1728  }
1729  
1730  /**
1731   * Purge all non-MUC caches not otherwise purged in purge_caches.
1732   *
1733   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1734   * {@link phpunit_util::reset_dataroot()}
1735   */
1736  function purge_other_caches() {
1737      global $DB, $CFG;
1738      if (class_exists('core_plugin_manager')) {
1739          core_plugin_manager::reset_caches();
1740      }
1741  
1742      // Bump up cacherev field for all courses.
1743      try {
1744          increment_revision_number('course', 'cacherev', '');
1745      } catch (moodle_exception $e) {
1746          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1747      }
1748  
1749      $DB->reset_caches();
1750  
1751      // Purge all other caches: rss, simplepie, etc.
1752      clearstatcache();
1753      remove_dir($CFG->cachedir.'', true);
1754  
1755      // Make sure cache dir is writable, throws exception if not.
1756      make_cache_directory('');
1757  
1758      // This is the only place where we purge local caches, we are only adding files there.
1759      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1760      remove_dir($CFG->localcachedir, true);
1761      set_config('localcachedirpurged', time());
1762      make_localcache_directory('', true);
1763      \core\task\manager::clear_static_caches();
1764  }
1765  
1766  /**
1767   * Get volatile flags
1768   *
1769   * @param string $type
1770   * @param int $changedsince default null
1771   * @return array records array
1772   */
1773  function get_cache_flags($type, $changedsince = null) {
1774      global $DB;
1775  
1776      $params = array('type' => $type, 'expiry' => time());
1777      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1778      if ($changedsince !== null) {
1779          $params['changedsince'] = $changedsince;
1780          $sqlwhere .= " AND timemodified > :changedsince";
1781      }
1782      $cf = array();
1783      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1784          foreach ($flags as $flag) {
1785              $cf[$flag->name] = $flag->value;
1786          }
1787      }
1788      return $cf;
1789  }
1790  
1791  /**
1792   * Get volatile flags
1793   *
1794   * @param string $type
1795   * @param string $name
1796   * @param int $changedsince default null
1797   * @return string|false The cache flag value or false
1798   */
1799  function get_cache_flag($type, $name, $changedsince=null) {
1800      global $DB;
1801  
1802      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1803  
1804      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1805      if ($changedsince !== null) {
1806          $params['changedsince'] = $changedsince;
1807          $sqlwhere .= " AND timemodified > :changedsince";
1808      }
1809  
1810      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1811  }
1812  
1813  /**
1814   * Set a volatile flag
1815   *
1816   * @param string $type the "type" namespace for the key
1817   * @param string $name the key to set
1818   * @param string $value the value to set (without magic quotes) - null will remove the flag
1819   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1820   * @return bool Always returns true
1821   */
1822  function set_cache_flag($type, $name, $value, $expiry = null) {
1823      global $DB;
1824  
1825      $timemodified = time();
1826      if ($expiry === null || $expiry < $timemodified) {
1827          $expiry = $timemodified + 24 * 60 * 60;
1828      } else {
1829          $expiry = (int)$expiry;
1830      }
1831  
1832      if ($value === null) {
1833          unset_cache_flag($type, $name);
1834          return true;
1835      }
1836  
1837      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1838          // This is a potential problem in DEBUG_DEVELOPER.
1839          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1840              return true; // No need to update.
1841          }
1842          $f->value        = $value;
1843          $f->expiry       = $expiry;
1844          $f->timemodified = $timemodified;
1845          $DB->update_record('cache_flags', $f);
1846      } else {
1847          $f = new stdClass();
1848          $f->flagtype     = $type;
1849          $f->name         = $name;
1850          $f->value        = $value;
1851          $f->expiry       = $expiry;
1852          $f->timemodified = $timemodified;
1853          $DB->insert_record('cache_flags', $f);
1854      }
1855      return true;
1856  }
1857  
1858  /**
1859   * Removes a single volatile flag
1860   *
1861   * @param string $type the "type" namespace for the key
1862   * @param string $name the key to set
1863   * @return bool
1864   */
1865  function unset_cache_flag($type, $name) {
1866      global $DB;
1867      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1868      return true;
1869  }
1870  
1871  /**
1872   * Garbage-collect volatile flags
1873   *
1874   * @return bool Always returns true
1875   */
1876  function gc_cache_flags() {
1877      global $DB;
1878      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1879      return true;
1880  }
1881  
1882  // USER PREFERENCE API.
1883  
1884  /**
1885   * Refresh user preference cache. This is used most often for $USER
1886   * object that is stored in session, but it also helps with performance in cron script.
1887   *
1888   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1889   *
1890   * @package  core
1891   * @category preference
1892   * @access   public
1893   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1894   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1895   * @throws   coding_exception
1896   * @return   null
1897   */
1898  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1899      global $DB;
1900      // Static cache, we need to check on each page load, not only every 2 minutes.
1901      static $loadedusers = array();
1902  
1903      if (!isset($user->id)) {
1904          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1905      }
1906  
1907      if (empty($user->id) or isguestuser($user->id)) {
1908          // No permanent storage for not-logged-in users and guest.
1909          if (!isset($user->preference)) {
1910              $user->preference = array();
1911          }
1912          return;
1913      }
1914  
1915      $timenow = time();
1916  
1917      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1918          // Already loaded at least once on this page. Are we up to date?
1919          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1920              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1921              return;
1922  
1923          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1924              // No change since the lastcheck on this page.
1925              $user->preference['_lastloaded'] = $timenow;
1926              return;
1927          }
1928      }
1929  
1930      // OK, so we have to reload all preferences.
1931      $loadedusers[$user->id] = true;
1932      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1933      $user->preference['_lastloaded'] = $timenow;
1934  }
1935  
1936  /**
1937   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1938   *
1939   * NOTE: internal function, do not call from other code.
1940   *
1941   * @package core
1942   * @access private
1943   * @param integer $userid the user whose prefs were changed.
1944   */
1945  function mark_user_preferences_changed($userid) {
1946      global $CFG;
1947  
1948      if (empty($userid) or isguestuser($userid)) {
1949          // No cache flags for guest and not-logged-in users.
1950          return;
1951      }
1952  
1953      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1954  }
1955  
1956  /**
1957   * Sets a preference for the specified user.
1958   *
1959   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1960   *
1961   * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1962   *
1963   * @package  core
1964   * @category preference
1965   * @access   public
1966   * @param    string            $name  The key to set as preference for the specified user
1967   * @param    string            $value The value to set for the $name key in the specified user's
1968   *                                    record, null means delete current value.
1969   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1970   * @throws   coding_exception
1971   * @return   bool                     Always true or exception
1972   */
1973  function set_user_preference($name, $value, $user = null) {
1974      global $USER, $DB;
1975  
1976      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1977          throw new coding_exception('Invalid preference name in set_user_preference() call');
1978      }
1979  
1980      if (is_null($value)) {
1981          // Null means delete current.
1982          return unset_user_preference($name, $user);
1983      } else if (is_object($value)) {
1984          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1985      } else if (is_array($value)) {
1986          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1987      }
1988      // Value column maximum length is 1333 characters.
1989      $value = (string)$value;
1990      if (core_text::strlen($value) > 1333) {
1991          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1992      }
1993  
1994      if (is_null($user)) {
1995          $user = $USER;
1996      } else if (isset($user->id)) {
1997          // It is a valid object.
1998      } else if (is_numeric($user)) {
1999          $user = (object)array('id' => (int)$user);
2000      } else {
2001          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
2002      }
2003  
2004      check_user_preferences_loaded($user);
2005  
2006      if (empty($user->id) or isguestuser($user->id)) {
2007          // No permanent storage for not-logged-in users and guest.
2008          $user->preference[$name] = $value;
2009          return true;
2010      }
2011  
2012      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
2013          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
2014              // Preference already set to this value.
2015              return true;
2016          }
2017          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
2018  
2019      } else {
2020          $preference = new stdClass();
2021          $preference->userid = $user->id;
2022          $preference->name   = $name;
2023          $preference->value  = $value;
2024          $DB->insert_record('user_preferences', $preference);
2025      }
2026  
2027      // Update value in cache.
2028      $user->preference[$name] = $value;
2029      // Update the $USER in case where we've not a direct reference to $USER.
2030      if ($user !== $USER && $user->id == $USER->id) {
2031          $USER->preference[$name] = $value;
2032      }
2033  
2034      // Set reload flag for other sessions.
2035      mark_user_preferences_changed($user->id);
2036  
2037      return true;
2038  }
2039  
2040  /**
2041   * Sets a whole array of preferences for the current user
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    array             $prefarray An array of key/value pairs to be set
2049   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
2050   * @return   bool                         Always true or exception
2051   */
2052  function set_user_preferences(array $prefarray, $user = null) {
2053      foreach ($prefarray as $name => $value) {
2054          set_user_preference($name, $value, $user);
2055      }
2056      return true;
2057  }
2058  
2059  /**
2060   * Unsets a preference completely by deleting it from the database
2061   *
2062   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2063   *
2064   * @package  core
2065   * @category preference
2066   * @access   public
2067   * @param    string            $name The key to unset as preference for the specified user
2068   * @param    stdClass|int|null $user A moodle user object or id, null means current user
2069   * @throws   coding_exception
2070   * @return   bool                    Always true or exception
2071   */
2072  function unset_user_preference($name, $user = null) {
2073      global $USER, $DB;
2074  
2075      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2076          throw new coding_exception('Invalid preference name in unset_user_preference() call');
2077      }
2078  
2079      if (is_null($user)) {
2080          $user = $USER;
2081      } else if (isset($user->id)) {
2082          // It is a valid object.
2083      } else if (is_numeric($user)) {
2084          $user = (object)array('id' => (int)$user);
2085      } else {
2086          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2087      }
2088  
2089      check_user_preferences_loaded($user);
2090  
2091      if (empty($user->id) or isguestuser($user->id)) {
2092          // No permanent storage for not-logged-in user and guest.
2093          unset($user->preference[$name]);
2094          return true;
2095      }
2096  
2097      // Delete from DB.
2098      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2099  
2100      // Delete the preference from cache.
2101      unset($user->preference[$name]);
2102      // Update the $USER in case where we've not a direct reference to $USER.
2103      if ($user !== $USER && $user->id == $USER->id) {
2104          unset($USER->preference[$name]);
2105      }
2106  
2107      // Set reload flag for other sessions.
2108      mark_user_preferences_changed($user->id);
2109  
2110      return true;
2111  }
2112  
2113  /**
2114   * Used to fetch user preference(s)
2115   *
2116   * If no arguments are supplied this function will return
2117   * all of the current user preferences as an array.
2118   *
2119   * If a name is specified then this function
2120   * attempts to return that particular preference value.  If
2121   * none is found, then the optional value $default is returned,
2122   * otherwise null.
2123   *
2124   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2125   *
2126   * @package  core
2127   * @category preference
2128   * @access   public
2129   * @param    string            $name    Name of the key to use in finding a preference value
2130   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
2131   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
2132   * @throws   coding_exception
2133   * @return   string|mixed|null          A string containing the value of a single preference. An
2134   *                                      array with all of the preferences or null
2135   */
2136  function get_user_preferences($name = null, $default = null, $user = null) {
2137      global $USER;
2138  
2139      if (is_null($name)) {
2140          // All prefs.
2141      } else if (is_numeric($name) or $name === '_lastloaded') {
2142          throw new coding_exception('Invalid preference name in get_user_preferences() call');
2143      }
2144  
2145      if (is_null($user)) {
2146          $user = $USER;
2147      } else if (isset($user->id)) {
2148          // Is a valid object.
2149      } else if (is_numeric($user)) {
2150          if ($USER->id == $user) {
2151              $user = $USER;
2152          } else {
2153              $user = (object)array('id' => (int)$user);
2154          }
2155      } else {
2156          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2157      }
2158  
2159      check_user_preferences_loaded($user);
2160  
2161      if (empty($name)) {
2162          // All values.
2163          return $user->preference;
2164      } else if (isset($user->preference[$name])) {
2165          // The single string value.
2166          return $user->preference[$name];
2167      } else {
2168          // Default value (null if not specified).
2169          return $default;
2170      }
2171  }
2172  
2173  // FUNCTIONS FOR HANDLING TIME.
2174  
2175  /**
2176   * Given Gregorian date parts in user time produce a GMT timestamp.
2177   *
2178   * @package core
2179   * @category time
2180   * @param int $year The year part to create timestamp of
2181   * @param int $month The month part to create timestamp of
2182   * @param int $day The day part to create timestamp of
2183   * @param int $hour The hour part to create timestamp of
2184   * @param int $minute The minute part to create timestamp of
2185   * @param int $second The second part to create timestamp of
2186   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2187   *             if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2188   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2189   *             applied only if timezone is 99 or string.
2190   * @return int GMT timestamp
2191   */
2192  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2193      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2194      $date->setDate((int)$year, (int)$month, (int)$day);
2195      $date->setTime((int)$hour, (int)$minute, (int)$second);
2196  
2197      $time = $date->getTimestamp();
2198  
2199      if ($time === false) {
2200          throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2201              ' This can fail if year is more than 2038 and OS is 32 bit windows');
2202      }
2203  
2204      // Moodle BC DST stuff.
2205      if (!$applydst) {
2206          $time += dst_offset_on($time, $timezone);
2207      }
2208  
2209      return $time;
2210  
2211  }
2212  
2213  /**
2214   * Format a date/time (seconds) as weeks, days, hours etc as needed
2215   *
2216   * Given an amount of time in seconds, returns string
2217   * formatted nicely as years, days, hours etc as needed
2218   *
2219   * @package core
2220   * @category time
2221   * @uses MINSECS
2222   * @uses HOURSECS
2223   * @uses DAYSECS
2224   * @uses YEARSECS
2225   * @param int $totalsecs Time in seconds
2226   * @param stdClass $str Should be a time object
2227   * @return string A nicely formatted date/time string
2228   */
2229  function format_time($totalsecs, $str = null) {
2230  
2231      $totalsecs = abs($totalsecs);
2232  
2233      if (!$str) {
2234          // Create the str structure the slow way.
2235          $str = new stdClass();
2236          $str->day   = get_string('day');
2237          $str->days  = get_string('days');
2238          $str->hour  = get_string('hour');
2239          $str->hours = get_string('hours');
2240          $str->min   = get_string('min');
2241          $str->mins  = get_string('mins');
2242          $str->sec   = get_string('sec');
2243          $str->secs  = get_string('secs');
2244          $str->year  = get_string('year');
2245          $str->years = get_string('years');
2246      }
2247  
2248      $years     = floor($totalsecs/YEARSECS);
2249      $remainder = $totalsecs - ($years*YEARSECS);
2250      $days      = floor($remainder/DAYSECS);
2251      $remainder = $totalsecs - ($days*DAYSECS);
2252      $hours     = floor($remainder/HOURSECS);
2253      $remainder = $remainder - ($hours*HOURSECS);
2254      $mins      = floor($remainder/MINSECS);
2255      $secs      = $remainder - ($mins*MINSECS);
2256  
2257      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2258      $sm = ($mins == 1)  ? $str->min  : $str->mins;
2259      $sh = ($hours == 1) ? $str->hour : $str->hours;
2260      $sd = ($days == 1)  ? $str->day  : $str->days;
2261      $sy = ($years == 1)  ? $str->year  : $str->years;
2262  
2263      $oyears = '';
2264      $odays = '';
2265      $ohours = '';
2266      $omins = '';
2267      $osecs = '';
2268  
2269      if ($years) {
2270          $oyears  = $years .' '. $sy;
2271      }
2272      if ($days) {
2273          $odays  = $days .' '. $sd;
2274      }
2275      if ($hours) {
2276          $ohours = $hours .' '. $sh;
2277      }
2278      if ($mins) {
2279          $omins  = $mins .' '. $sm;
2280      }
2281      if ($secs) {
2282          $osecs  = $secs .' '. $ss;
2283      }
2284  
2285      if ($years) {
2286          return trim($oyears .' '. $odays);
2287      }
2288      if ($days) {
2289          return trim($odays .' '. $ohours);
2290      }
2291      if ($hours) {
2292          return trim($ohours .' '. $omins);
2293      }
2294      if ($mins) {
2295          return trim($omins .' '. $osecs);
2296      }
2297      if ($secs) {
2298          return $osecs;
2299      }
2300      return get_string('now');
2301  }
2302  
2303  /**
2304   * Returns a formatted string that represents a date in user time.
2305   *
2306   * @package core
2307   * @category time
2308   * @param int $date the timestamp in UTC, as obtained from the database.
2309   * @param string $format strftime format. You should probably get this using
2310   *        get_string('strftime...', 'langconfig');
2311   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2312   *        not 99 then daylight saving will not be added.
2313   *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2314   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2315   *        If false then the leading zero is maintained.
2316   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2317   * @return string the formatted date/time.
2318   */
2319  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2320      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2321      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2322  }
2323  
2324  /**
2325   * Returns a html "time" tag with both the exact user date with timezone information
2326   * as a datetime attribute in the W3C format, and the user readable date and time as text.
2327   *
2328   * @package core
2329   * @category time
2330   * @param int $date the timestamp in UTC, as obtained from the database.
2331   * @param string $format strftime format. You should probably get this using
2332   *        get_string('strftime...', 'langconfig');
2333   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2334   *        not 99 then daylight saving will not be added.
2335   *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2336   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2337   *        If false then the leading zero is maintained.
2338   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2339   * @return string the formatted date/time.
2340   */
2341  function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2342      $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2343      if (CLI_SCRIPT && !PHPUNIT_TEST) {
2344          return $userdatestr;
2345      }
2346      $machinedate = new DateTime();
2347      $machinedate->setTimestamp(intval($date));
2348      $machinedate->setTimezone(core_date::get_user_timezone_object());
2349  
2350      return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2351  }
2352  
2353  /**
2354   * Returns a formatted date ensuring it is UTF-8.
2355   *
2356   * If we are running under Windows convert to Windows encoding and then back to UTF-8
2357   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2358   *
2359   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2360   * @param string $format strftime format.
2361   * @param int|float|string $tz the user timezone
2362   * @return string the formatted date/time.
2363   * @since Moodle 2.3.3
2364   */
2365  function date_format_string($date, $format, $tz = 99) {
2366  
2367      date_default_timezone_set(core_date::get_user_timezone($tz));
2368  
2369      if (date('A', 0) === date('A', HOURSECS * 18)) {
2370          $datearray = getdate($date);
2371          $format = str_replace([
2372              '%P',
2373              '%p',
2374          ], [
2375              $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
2376              $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
2377          ], $format);
2378      }
2379  
2380      $datestring = core_date::strftime($format, $date);
2381      core_date::set_default_server_timezone();
2382  
2383      return $datestring;
2384  }
2385  
2386  /**
2387   * Given a $time timestamp in GMT (seconds since epoch),
2388   * returns an array that represents the Gregorian date in user time
2389   *
2390   * @package core
2391   * @category time
2392   * @param int $time Timestamp in GMT
2393   * @param float|int|string $timezone user timezone
2394   * @return array An array that represents the date in user time
2395   */
2396  function usergetdate($time, $timezone=99) {
2397      if ($time === null) {
2398          // PHP8 and PHP7 return different results when getdate(null) is called.
2399          // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
2400          // In the future versions of Moodle we may consider adding a strict typehint.
2401          debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
2402          $time = 0;
2403      }
2404  
2405      date_default_timezone_set(core_date::get_user_timezone($timezone));
2406      $result = getdate($time);
2407      core_date::set_default_server_timezone();
2408  
2409      return $result;
2410  }
2411  
2412  /**
2413   * Given a GMT timestamp (seconds since epoch), offsets it by
2414   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2415   *
2416   * NOTE: this function does not include DST properly,
2417   *       you should use the PHP date stuff instead!
2418   *
2419   * @package core
2420   * @category time
2421   * @param int $date Timestamp in GMT
2422   * @param float|int|string $timezone user timezone
2423   * @return int
2424   */
2425  function usertime($date, $timezone=99) {
2426      $userdate = new DateTime('@' . $date);
2427      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2428      $dst = dst_offset_on($date, $timezone);
2429  
2430      return $date - $userdate->getOffset() + $dst;
2431  }
2432  
2433  /**
2434   * Get a formatted string representation of an interval between two unix timestamps.
2435   *
2436   * E.g.
2437   * $intervalstring = get_time_interval_string(12345600, 12345660);
2438   * Will produce the string:
2439   * '0d 0h 1m'
2440   *
2441   * @param int $time1 unix timestamp
2442   * @param int $time2 unix timestamp
2443   * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2444   * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2445   */
2446  function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
2447      $dtdate = new DateTime();
2448      $dtdate->setTimeStamp($time1);
2449      $dtdate2 = new DateTime();
2450      $dtdate2->setTimeStamp($time2);
2451      $interval = $dtdate2->diff($dtdate);
2452      $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
2453      return $interval->format($format);
2454  }
2455  
2456  /**
2457   * Given a time, return the GMT timestamp of the most recent midnight
2458   * for the current user.
2459   *
2460   * @package core
2461   * @category time
2462   * @param int $date Timestamp in GMT
2463   * @param float|int|string $timezone user timezone
2464   * @return int Returns a GMT timestamp
2465   */
2466  function usergetmidnight($date, $timezone=99) {
2467  
2468      $userdate = usergetdate($date, $timezone);
2469  
2470      // Time of midnight of this user's day, in GMT.
2471      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2472  
2473  }
2474  
2475  /**
2476   * Returns a string that prints the user's timezone
2477   *
2478   * @package core
2479   * @category time
2480   * @param float|int|string $timezone user timezone
2481   * @return string
2482   */
2483  function usertimezone($timezone=99) {
2484      $tz = core_date::get_user_timezone($timezone);
2485      return core_date::get_localised_timezone($tz);
2486  }
2487  
2488  /**
2489   * Returns a float or a string which denotes the user's timezone
2490   * 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)
2491   * means that for this timezone there are also DST rules to be taken into account
2492   * Checks various settings and picks the most dominant of those which have a value
2493   *
2494   * @package core
2495   * @category time
2496   * @param float|int|string $tz timezone to calculate GMT time offset before
2497   *        calculating user timezone, 99 is default user timezone
2498   *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2499   * @return float|string
2500   */
2501  function get_user_timezone($tz = 99) {
2502      global $USER, $CFG;
2503  
2504      $timezones = array(
2505          $tz,
2506          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2507          isset($USER->timezone) ? $USER->timezone : 99,
2508          isset($CFG->timezone) ? $CFG->timezone : 99,
2509          );
2510  
2511      $tz = 99;
2512  
2513      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2514      foreach ($timezones as $nextvalue) {
2515          if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2516              $tz = $nextvalue;
2517          }
2518      }
2519      return is_numeric($tz) ? (float) $tz : $tz;
2520  }
2521  
2522  /**
2523   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2524   * - Note: Daylight saving only works for string timezones and not for float.
2525   *
2526   * @package core
2527   * @category time
2528   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2529   * @param int|float|string $strtimezone user timezone
2530   * @return int
2531   */
2532  function dst_offset_on($time, $strtimezone = null) {
2533      $tz = core_date::get_user_timezone($strtimezone);
2534      $date = new DateTime('@' . $time);
2535      $date->setTimezone(new DateTimeZone($tz));
2536      if ($date->format('I') == '1') {
2537          if ($tz === 'Australia/Lord_Howe') {
2538              return 1800;
2539          }
2540          return 3600;
2541      }
2542      return 0;
2543  }
2544  
2545  /**
2546   * Calculates when the day appears in specific month
2547   *
2548   * @package core
2549   * @category time
2550   * @param int $startday starting day of the month
2551   * @param int $weekday The day when week starts (normally taken from user preferences)
2552   * @param int $month The month whose day is sought
2553   * @param int $year The year of the month whose day is sought
2554   * @return int
2555   */
2556  function find_day_in_month($startday, $weekday, $month, $year) {
2557      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2558  
2559      $daysinmonth = days_in_month($month, $year);
2560      $daysinweek = count($calendartype->get_weekdays());
2561  
2562      if ($weekday == -1) {
2563          // Don't care about weekday, so return:
2564          //    abs($startday) if $startday != -1
2565          //    $daysinmonth otherwise.
2566          return ($startday == -1) ? $daysinmonth : abs($startday);
2567      }
2568  
2569      // From now on we 're looking for a specific weekday.
2570      // Give "end of month" its actual value, since we know it.
2571      if ($startday == -1) {
2572          $startday = -1 * $daysinmonth;
2573      }
2574  
2575      // Starting from day $startday, the sign is the direction.
2576      if ($startday < 1) {
2577          $startday = abs($startday);
2578          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2579  
2580          // This is the last such weekday of the month.
2581          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2582          if ($lastinmonth > $daysinmonth) {
2583              $lastinmonth -= $daysinweek;
2584          }
2585  
2586          // Find the first such weekday <= $startday.
2587          while ($lastinmonth > $startday) {
2588              $lastinmonth -= $daysinweek;
2589          }
2590  
2591          return $lastinmonth;
2592      } else {
2593          $indexweekday = dayofweek($startday, $month, $year);
2594  
2595          $diff = $weekday - $indexweekday;
2596          if ($diff < 0) {
2597              $diff += $daysinweek;
2598          }
2599  
2600          // This is the first such weekday of the month equal to or after $startday.
2601          $firstfromindex = $startday + $diff;
2602  
2603          return $firstfromindex;
2604      }
2605  }
2606  
2607  /**
2608   * Calculate the number of days in a given month
2609   *
2610   * @package core
2611   * @category time
2612   * @param int $month The month whose day count is sought
2613   * @param int $year The year of the month whose day count is sought
2614   * @return int
2615   */
2616  function days_in_month($month, $year) {
2617      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2618      return $calendartype->get_num_days_in_month($year, $month);
2619  }
2620  
2621  /**
2622   * Calculate the position in the week of a specific calendar day
2623   *
2624   * @package core
2625   * @category time
2626   * @param int $day The day of the date whose position in the week is sought
2627   * @param int $month The month of the date whose position in the week is sought
2628   * @param int $year The year of the date whose position in the week is sought
2629   * @return int
2630   */
2631  function dayofweek($day, $month, $year) {
2632      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2633      return $calendartype->get_weekday($year, $month, $day);
2634  }
2635  
2636  // USER AUTHENTICATION AND LOGIN.
2637  
2638  /**
2639   * Returns full login url.
2640   *
2641   * Any form submissions for authentication to this URL must include username,
2642   * password as well as a logintoken generated by \core\session\manager::get_login_token().
2643   *
2644   * @return string login url
2645   */
2646  function get_login_url() {
2647      global $CFG;
2648  
2649      return "$CFG->wwwroot/login/index.php";
2650  }
2651  
2652  /**
2653   * This function checks that the current user is logged in and has the
2654   * required privileges
2655   *
2656   * This function checks that the current user is logged in, and optionally
2657   * whether they are allowed to be in a particular course and view a particular
2658   * course module.
2659   * If they are not logged in, then it redirects them to the site login unless
2660   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2661   * case they are automatically logged in as guests.
2662   * If $courseid is given and the user is not enrolled in that course then the
2663   * user is redirected to the course enrolment page.
2664   * If $cm is given and the course module is hidden and the user is not a teacher
2665   * in the course then the user is redirected to the course home page.
2666   *
2667   * When $cm parameter specified, this function sets page layout to 'module'.
2668   * You need to change it manually later if some other layout needed.
2669   *
2670   * @package    core_access
2671   * @category   access
2672   *
2673   * @param mixed $courseorid id of the course or course object
2674   * @param bool $autologinguest default true
2675   * @param object $cm course module object
2676   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2677   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2678   *             in order to keep redirects working properly. MDL-14495
2679   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2680   * @return mixed Void, exit, and die depending on path
2681   * @throws coding_exception
2682   * @throws require_login_exception
2683   * @throws moodle_exception
2684   */
2685  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2686      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2687  
2688      // Must not redirect when byteserving already started.
2689      if (!empty($_SERVER['HTTP_RANGE'])) {
2690          $preventredirect = true;
2691      }
2692  
2693      if (AJAX_SCRIPT) {
2694          // We cannot redirect for AJAX scripts either.
2695          $preventredirect = true;
2696      }
2697  
2698      // Setup global $COURSE, themes, language and locale.
2699      if (!empty($courseorid)) {
2700          if (is_object($courseorid)) {
2701              $course = $courseorid;
2702          } else if ($courseorid == SITEID) {
2703              $course = clone($SITE);
2704          } else {
2705              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2706          }
2707          if ($cm) {
2708              if ($cm->course != $course->id) {
2709                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2710              }
2711              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2712              if (!($cm instanceof cm_info)) {
2713                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2714                  // db queries so this is not really a performance concern, however it is obviously
2715                  // better if you use get_fast_modinfo to get the cm before calling this.
2716                  $modinfo = get_fast_modinfo($course);
2717                  $cm = $modinfo->get_cm($cm->id);
2718              }
2719          }
2720      } else {
2721          // Do not touch global $COURSE via $PAGE->set_course(),
2722          // the reasons is we need to be able to call require_login() at any time!!
2723          $course = $SITE;
2724          if ($cm) {
2725              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2726          }
2727      }
2728  
2729      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2730      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2731      // risk leading the user back to the AJAX request URL.
2732      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2733          $setwantsurltome = false;
2734      }
2735  
2736      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2737      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2738          if ($preventredirect) {
2739              throw new require_login_session_timeout_exception();
2740          } else {
2741              if ($setwantsurltome) {
2742                  $SESSION->wantsurl = qualified_me();
2743              }
2744              redirect(get_login_url());
2745          }
2746      }
2747  
2748      // If the user is not even logged in yet then make sure they are.
2749      if (!isloggedin()) {
2750          if ($autologinguest && !empty($CFG->autologinguests)) {
2751              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2752                  // Misconfigured site guest, just redirect to login page.
2753                  redirect(get_login_url());
2754                  exit; // Never reached.
2755              }
2756              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2757              complete_user_login($guest);
2758              $USER->autologinguest = true;
2759              $SESSION->lang = $lang;
2760          } else {
2761              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2762              if ($preventredirect) {
2763                  throw new require_login_exception('You are not logged in');
2764              }
2765  
2766              if ($setwantsurltome) {
2767                  $SESSION->wantsurl = qualified_me();
2768              }
2769  
2770              // Give auth plugins an opportunity to authenticate or redirect to an external login page
2771              $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2772              foreach($authsequence as $authname) {
2773                  $authplugin = get_auth_plugin($authname);
2774                  $authplugin->pre_loginpage_hook();
2775                  if (isloggedin()) {
2776                      if ($cm) {
2777                          $modinfo = get_fast_modinfo($course);
2778                          $cm = $modinfo->get_cm($cm->id);
2779                      }
2780                      set_access_log_user();
2781                      break;
2782                  }
2783              }
2784  
2785              // If we're still not logged in then go to the login page
2786              if (!isloggedin()) {
2787                  redirect(get_login_url());
2788                  exit; // Never reached.
2789              }
2790          }
2791      }
2792  
2793      // Loginas as redirection if needed.
2794      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2795          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2796              if ($USER->loginascontext->instanceid != $course->id) {
2797                  throw new \moodle_exception('loginasonecourse', '',
2798                      $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2799              }
2800          }
2801      }
2802  
2803      // Check whether the user should be changing password (but only if it is REALLY them).
2804      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2805          $userauth = get_auth_plugin($USER->auth);
2806          if ($userauth->can_change_password() and !$preventredirect) {
2807              if ($setwantsurltome) {
2808                  $SESSION->wantsurl = qualified_me();
2809              }
2810              if ($changeurl = $userauth->change_password_url()) {
2811                  // Use plugin custom url.
2812                  redirect($changeurl);
2813              } else {
2814                  // Use moodle internal method.
2815                  redirect($CFG->wwwroot .'/login/change_password.php');
2816              }
2817          } else if ($userauth->can_change_password()) {
2818              throw new moodle_exception('forcepasswordchangenotice');
2819          } else {
2820              throw new moodle_exception('nopasswordchangeforced', 'auth');
2821          }
2822      }
2823  
2824      // Check that the user account is properly set up. If we can't redirect to
2825      // edit their profile and this is not a WS request, perform just the lax check.
2826      // It will allow them to use filepicker on the profile edit page.
2827  
2828      if ($preventredirect && !WS_SERVER) {
2829          $usernotfullysetup = user_not_fully_set_up($USER, false);
2830      } else {
2831          $usernotfullysetup = user_not_fully_set_up($USER, true);
2832      }
2833  
2834      if ($usernotfullysetup) {
2835          if ($preventredirect) {
2836              throw new moodle_exception('usernotfullysetup');
2837          }
2838          if ($setwantsurltome) {
2839              $SESSION->wantsurl = qualified_me();
2840          }
2841          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2842      }
2843  
2844      // Make sure the USER has a sesskey set up. Used for CSRF protection.
2845      sesskey();
2846  
2847      if (\core\session\manager::is_loggedinas()) {
2848          // During a "logged in as" session we should force all content to be cleaned because the
2849          // logged in user will be viewing potentially malicious user generated content.
2850          // See MDL-63786 for more details.
2851          $CFG->forceclean = true;
2852      }
2853  
2854      $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2855  
2856      // Do not bother admins with any formalities, except for activities pending deletion.
2857      if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2858          // Set the global $COURSE.
2859          if ($cm) {
2860              $PAGE->set_cm($cm, $course);
2861              $PAGE->set_pagelayout('incourse');
2862          } else if (!empty($courseorid)) {
2863              $PAGE->set_course($course);
2864          }
2865          // Set accesstime or the user will appear offline which messes up messaging.
2866          // Do not update access time for webservice or ajax requests.
2867          if (!WS_SERVER && !AJAX_SCRIPT) {
2868              user_accesstime_log($course->id);
2869          }
2870  
2871          foreach ($afterlogins as $plugintype => $plugins) {
2872              foreach ($plugins as $pluginfunction) {
2873                  $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2874              }
2875          }
2876          return;
2877      }
2878  
2879      // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2880      // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2881      if (!defined('NO_SITEPOLICY_CHECK')) {
2882          define('NO_SITEPOLICY_CHECK', false);
2883      }
2884  
2885      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2886      // Do not test if the script explicitly asked for skipping the site policies check.
2887      // Or if the user auth type is webservice.
2888      if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2889          $manager = new \core_privacy\local\sitepolicy\manager();
2890          if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2891              if ($preventredirect) {
2892                  throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2893              }
2894              if ($setwantsurltome) {
2895                  $SESSION->wantsurl = qualified_me();
2896              }
2897              redirect($policyurl);
2898          }
2899      }
2900  
2901      // Fetch the system context, the course context, and prefetch its child contexts.
2902      $sysctx = context_system::instance();
2903      $coursecontext = context_course::instance($course->id, MUST_EXIST);
2904      if ($cm) {
2905          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2906      } else {
2907          $cmcontext = null;
2908      }
2909  
2910      // If the site is currently under maintenance, then print a message.
2911      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2912          if ($preventredirect) {
2913              throw new require_login_exception('Maintenance in progress');
2914          }
2915          $PAGE->set_context(null);
2916          print_maintenance_message();
2917      }
2918  
2919      // Make sure the course itself is not hidden.
2920      if ($course->id == SITEID) {
2921          // Frontpage can not be hidden.
2922      } else {
2923          if (is_role_switched($course->id)) {
2924              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2925          } else {
2926              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2927                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2928                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2929                  if ($preventredirect) {
2930                      throw new require_login_exception('Course is hidden');
2931                  }
2932                  $PAGE->set_context(null);
2933                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
2934                  // the navigation will mess up when trying to find it.
2935                  navigation_node::override_active_url(new moodle_url('/'));
2936                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2937              }
2938          }
2939      }
2940  
2941      // Is the user enrolled?
2942      if ($course->id == SITEID) {
2943          // Everybody is enrolled on the frontpage.
2944      } else {
2945          if (\core\session\manager::is_loggedinas()) {
2946              // Make sure the REAL person can access this course first.
2947              $realuser = \core\session\manager::get_realuser();
2948              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2949                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2950                  if ($preventredirect) {
2951                      throw new require_login_exception('Invalid course login-as access');
2952                  }
2953                  $PAGE->set_context(null);
2954                  echo $OUTPUT->header();
2955                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2956              }
2957          }
2958  
2959          $access = false;
2960  
2961          if (is_role_switched($course->id)) {
2962              // Ok, user had to be inside this course before the switch.
2963              $access = true;
2964  
2965          } else if (is_viewing($coursecontext, $USER)) {
2966              // Ok, no need to mess with enrol.
2967              $access = true;
2968  
2969          } else {
2970              if (isset($USER->enrol['enrolled'][$course->id])) {
2971                  if ($USER->enrol['enrolled'][$course->id] > time()) {
2972                      $access = true;
2973                      if (isset($USER->enrol['tempguest'][$course->id])) {
2974                          unset($USER->enrol['tempguest'][$course->id]);
2975                          remove_temp_course_roles($coursecontext);
2976                      }
2977                  } else {
2978                      // Expired.
2979                      unset($USER->enrol['enrolled'][$course->id]);
2980                  }
2981              }
2982              if (isset($USER->enrol['tempguest'][$course->id])) {
2983                  if ($USER->enrol['tempguest'][$course->id] == 0) {
2984                      $access = true;
2985                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2986                      $access = true;
2987                  } else {
2988                      // Expired.
2989                      unset($USER->enrol['tempguest'][$course->id]);
2990                      remove_temp_course_roles($coursecontext);
2991                  }
2992              }
2993  
2994              if (!$access) {
2995                  // Cache not ok.
2996                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2997                  if ($until !== false) {
2998                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
2999                      if ($until == 0) {
3000                          $until = ENROL_MAX_TIMESTAMP;
3001                      }
3002                      $USER->enrol['enrolled'][$course->id] = $until;
3003                      $access = true;
3004  
3005                  } else if (core_course_category::can_view_course_info($course)) {
3006                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
3007                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3008                      $enrols = enrol_get_plugins(true);
3009                      // First ask all enabled enrol instances in course if they want to auto enrol user.
3010                      foreach ($instances as $instance) {
3011                          if (!isset($enrols[$instance->enrol])) {
3012                              continue;
3013                          }
3014                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3015                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3016                          if ($until !== false) {
3017                              if ($until == 0) {
3018                                  $until = ENROL_MAX_TIMESTAMP;
3019                              }
3020                              $USER->enrol['enrolled'][$course->id] = $until;
3021                              $access = true;
3022                              break;
3023                          }
3024                      }
3025                      // If not enrolled yet try to gain temporary guest access.
3026                      if (!$access) {
3027                          foreach ($instances as $instance) {
3028                              if (!isset($enrols[$instance->enrol])) {
3029                                  continue;
3030                              }
3031                              // Get a duration for the guest access, a timestamp in the future or false.
3032                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3033                              if ($until !== false and $until > time()) {
3034                                  $USER->enrol['tempguest'][$course->id] = $until;
3035                                  $access = true;
3036                                  break;
3037                              }
3038                          }
3039                      }
3040                  } else {
3041                      // User is not enrolled and is not allowed to browse courses here.
3042                      if ($preventredirect) {
3043                          throw new require_login_exception('Course is not available');
3044                      }
3045                      $PAGE->set_context(null);
3046                      // We need to override the navigation URL as the course won't have been added to the navigation and thus
3047                      // the navigation will mess up when trying to find it.
3048                      navigation_node::override_active_url(new moodle_url('/'));
3049                      notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3050                  }
3051              }
3052          }
3053  
3054          if (!$access) {
3055              if ($preventredirect) {
3056                  throw new require_login_exception('Not enrolled');
3057              }
3058              if ($setwantsurltome) {
3059                  $SESSION->wantsurl = qualified_me();
3060              }
3061              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3062          }
3063      }
3064  
3065      // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3066      if ($cm && $cm->deletioninprogress) {
3067          if ($preventredirect) {
3068              throw new moodle_exception('activityisscheduledfordeletion');
3069          }
3070          require_once($CFG->dirroot . '/course/lib.php');
3071          redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3072      }
3073  
3074      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3075      if ($cm && !$cm->uservisible) {
3076          if ($preventredirect) {
3077              throw new require_login_exception('Activity is hidden');
3078          }
3079          // Get the error message that activity is not available and why (if explanation can be shown to the user).
3080          $PAGE->set_course($course);
3081          $renderer = $PAGE->get_renderer('course');
3082          $message = $renderer->course_section_cm_unavailable_error_message($cm);
3083          redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3084      }
3085  
3086      // Set the global $COURSE.
3087      if ($cm) {
3088          $PAGE->set_cm($cm, $course);
3089          $PAGE->set_pagelayout('incourse');
3090      } else if (!empty($courseorid)) {
3091          $PAGE->set_course($course);
3092      }
3093  
3094      foreach ($afterlogins as $plugintype => $plugins) {
3095          foreach ($plugins as $pluginfunction) {
3096              $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3097          }
3098      }
3099  
3100      // Finally access granted, update lastaccess times.
3101      // Do not update access time for webservice or ajax requests.
3102      if (!WS_SERVER && !AJAX_SCRIPT) {
3103          user_accesstime_log($course->id);
3104      }
3105  }
3106  
3107  /**
3108   * A convenience function for where we must be logged in as admin
3109   * @return void
3110   */
3111  function require_admin() {
3112      require_login(null, false);
3113      require_capability('moodle/site:config', context_system::instance());
3114  }
3115  
3116  /**
3117   * This function just makes sure a user is logged out.
3118   *
3119   * @package    core_access
3120   * @category   access
3121   */
3122  function require_logout() {
3123      global $USER, $DB;
3124  
3125      if (!isloggedin()) {
3126          // This should not happen often, no need for hooks or events here.
3127          \core\session\manager::terminate_current();
3128          return;
3129      }
3130  
3131      // Execute hooks before action.
3132      $authplugins = array();
3133      $authsequence = get_enabled_auth_plugins();
3134      foreach ($authsequence as $authname) {
3135          $authplugins[$authname] = get_auth_plugin($authname);
3136          $authplugins[$authname]->prelogout_hook();
3137      }
3138  
3139      // Store info that gets removed during logout.
3140      $sid = session_id();
3141      $event = \core\event\user_loggedout::create(
3142          array(
3143              'userid' => $USER->id,
3144              'objectid' => $USER->id,
3145              'other' => array('sessionid' => $sid),
3146          )
3147      );
3148      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3149          $event->add_record_snapshot('sessions', $session);
3150      }
3151  
3152      // Clone of $USER object to be used by auth plugins.
3153      $user = fullclone($USER);
3154  
3155      // Delete session record and drop $_SESSION content.
3156      \core\session\manager::terminate_current();
3157  
3158      // Trigger event AFTER action.
3159      $event->trigger();
3160  
3161      // Hook to execute auth plugins redirection after event trigger.
3162      foreach ($authplugins as $authplugin) {
3163          $authplugin->postlogout_hook($user);
3164      }
3165  }
3166  
3167  /**
3168   * Weaker version of require_login()
3169   *
3170   * This is a weaker version of {@link require_login()} which only requires login
3171   * when called from within a course rather than the site page, unless
3172   * the forcelogin option is turned on.
3173   * @see require_login()
3174   *
3175   * @package    core_access
3176   * @category   access
3177   *
3178   * @param mixed $courseorid The course object or id in question
3179   * @param bool $autologinguest Allow autologin guests if that is wanted
3180   * @param object $cm Course activity module if known
3181   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3182   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3183   *             in order to keep redirects working properly. MDL-14495
3184   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3185   * @return void
3186   * @throws coding_exception
3187   */
3188  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3189      global $CFG, $PAGE, $SITE;
3190      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3191            or (!is_object($courseorid) and $courseorid == SITEID));
3192      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3193          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3194          // db queries so this is not really a performance concern, however it is obviously
3195          // better if you use get_fast_modinfo to get the cm before calling this.
3196          if (is_object($courseorid)) {
3197              $course = $courseorid;
3198          } else {
3199              $course = clone($SITE);
3200          }
3201          $modinfo = get_fast_modinfo($course);
3202          $cm = $modinfo->get_cm($cm->id);
3203      }
3204      if (!empty($CFG->forcelogin)) {
3205          // Login required for both SITE and courses.
3206          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3207  
3208      } else if ($issite && !empty($cm) and !$cm->uservisible) {
3209          // Always login for hidden activities.
3210          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3211  
3212      } else if (isloggedin() && !isguestuser()) {
3213          // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3214          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3215  
3216      } else if ($issite) {
3217          // Login for SITE not required.
3218          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3219          if (!empty($courseorid)) {
3220              if (is_object($courseorid)) {
3221                  $course = $courseorid;
3222              } else {
3223                  $course = clone $SITE;
3224              }
3225              if ($cm) {
3226                  if ($cm->course != $course->id) {
3227                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3228                  }
3229                  $PAGE->set_cm($cm, $course);
3230                  $PAGE->set_pagelayout('incourse');
3231              } else {
3232                  $PAGE->set_course($course);
3233              }
3234          } else {
3235              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3236              $PAGE->set_course($PAGE->course);
3237          }
3238          // Do not update access time for webservice or ajax requests.
3239          if (!WS_SERVER && !AJAX_SCRIPT) {
3240              user_accesstime_log(SITEID);
3241          }
3242          return;
3243  
3244      } else {
3245          // Course login always required.
3246          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3247      }
3248  }
3249  
3250  /**
3251   * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3252   *
3253   * @param  string $keyvalue the key value
3254   * @param  string $script   unique script identifier
3255   * @param  int $instance    instance id
3256   * @return stdClass the key entry in the user_private_key table
3257   * @since Moodle 3.2
3258   * @throws moodle_exception
3259   */
3260  function validate_user_key($keyvalue, $script, $instance) {
3261      global $DB;
3262  
3263      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3264          throw new \moodle_exception('invalidkey');
3265      }
3266  
3267      if (!empty($key->validuntil) and $key->validuntil < time()) {
3268          throw new \moodle_exception('expiredkey');
3269      }
3270  
3271      if ($key->iprestriction) {
3272          $remoteaddr = getremoteaddr(null);
3273          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3274              throw new \moodle_exception('ipmismatch');
3275          }
3276      }
3277      return $key;
3278  }
3279  
3280  /**
3281   * Require key login. Function terminates with error if key not found or incorrect.
3282   *
3283   * @uses NO_MOODLE_COOKIES
3284   * @uses PARAM_ALPHANUM
3285   * @param string $script unique script identifier
3286   * @param int $instance optional instance id
3287   * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3288   * @return int Instance ID
3289   */
3290  function require_user_key_login($script, $instance = null, $keyvalue = null) {
3291      global $DB;
3292  
3293      if (!NO_MOODLE_COOKIES) {
3294          throw new \moodle_exception('sessioncookiesdisable');
3295      }
3296  
3297      // Extra safety.
3298      \core\session\manager::write_close();
3299  
3300      if (null === $keyvalue) {
3301          $keyvalue = required_param('key', PARAM_ALPHANUM);
3302      }
3303  
3304      $key = validate_user_key($keyvalue, $script, $instance);
3305  
3306      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3307          throw new \moodle_exception('invaliduserid');
3308      }
3309  
3310      core_user::require_active_user($user, true, true);
3311  
3312      // Emulate normal session.
3313      enrol_check_plugins($user, false);
3314      \core\session\manager::set_user($user);
3315  
3316      // Note we are not using normal login.
3317      if (!defined('USER_KEY_LOGIN')) {
3318          define('USER_KEY_LOGIN', true);
3319      }
3320  
3321      // Return instance id - it might be empty.
3322      return $key->instance;
3323  }
3324  
3325  /**
3326   * Creates a new private user access key.
3327   *
3328   * @param string $script unique target identifier
3329   * @param int $userid
3330   * @param int $instance optional instance id
3331   * @param string $iprestriction optional ip restricted access
3332   * @param int $validuntil key valid only until given data
3333   * @return string access key value
3334   */
3335  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3336      global $DB;
3337  
3338      $key = new stdClass();
3339      $key->script        = $script;
3340      $key->userid        = $userid;
3341      $key->instance      = $instance;
3342      $key->iprestriction = $iprestriction;
3343      $key->validuntil    = $validuntil;
3344      $key->timecreated   = time();
3345  
3346      // Something long and unique.
3347      $key->value         = md5($userid.'_'.time().random_string(40));
3348      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3349          // Must be unique.
3350          $key->value     = md5($userid.'_'.time().random_string(40));
3351      }
3352      $DB->insert_record('user_private_key', $key);
3353      return $key->value;
3354  }
3355  
3356  /**
3357   * Delete the user's new private user access keys for a particular script.
3358   *
3359   * @param string $script unique target identifier
3360   * @param int $userid
3361   * @return void
3362   */
3363  function delete_user_key($script, $userid) {
3364      global $DB;
3365      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3366  }
3367  
3368  /**
3369   * Gets a private user access key (and creates one if one doesn't exist).
3370   *
3371   * @param string $script unique target identifier
3372   * @param int $userid
3373   * @param int $instance optional instance id
3374   * @param string $iprestriction optional ip restricted access
3375   * @param int $validuntil key valid only until given date
3376   * @return string access key value
3377   */
3378  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3379      global $DB;
3380  
3381      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3382                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
3383                                                           'validuntil' => $validuntil))) {
3384          return $key->value;
3385      } else {
3386          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3387      }
3388  }
3389  
3390  
3391  /**
3392   * Modify the user table by setting the currently logged in user's last login to now.
3393   *
3394   * @return bool Always returns true
3395   */
3396  function update_user_login_times() {
3397      global $USER, $DB, $SESSION;
3398  
3399      if (isguestuser()) {
3400          // Do not update guest access times/ips for performance.
3401          return true;
3402      }
3403  
3404      if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
3405          // Do not update user login time when using user key login.
3406          return true;
3407      }
3408  
3409      $now = time();
3410  
3411      $user = new stdClass();
3412      $user->id = $USER->id;
3413  
3414      // Make sure all users that logged in have some firstaccess.
3415      if ($USER->firstaccess == 0) {
3416          $USER->firstaccess = $user->firstaccess = $now;
3417      }
3418  
3419      // Store the previous current as lastlogin.
3420      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3421  
3422      $USER->currentlogin = $user->currentlogin = $now;
3423  
3424      // Function user_accesstime_log() may not update immediately, better do it here.
3425      $USER->lastaccess = $user->lastaccess = $now;
3426      $SESSION->userpreviousip = $USER->lastip;
3427      $USER->lastip = $user->lastip = getremoteaddr();
3428  
3429      // Note: do not call user_update_user() here because this is part of the login process,
3430      //       the login event means that these fields were updated.
3431      $DB->update_record('user', $user);
3432      return true;
3433  }
3434  
3435  /**
3436   * Determines if a user has completed setting up their account.
3437   *
3438   * The lax mode (with $strict = false) has been introduced for special cases
3439   * only where we want to skip certain checks intentionally. This is valid in
3440   * certain mnet or ajax scenarios when the user cannot / should not be
3441   * redirected to edit their profile. In most cases, you should perform the
3442   * strict check.
3443   *
3444   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3445   * @param bool $strict Be more strict and assert id and custom profile fields set, too
3446   * @return bool
3447   */
3448  function user_not_fully_set_up($user, $strict = true) {
3449      global $CFG, $SESSION, $USER;
3450      require_once($CFG->dirroot.'/user/profile/lib.php');
3451  
3452      // If the user is setup then store this in the session to avoid re-checking.
3453      // Some edge cases are when the users email starts to bounce or the
3454      // configuration for custom fields has changed while they are logged in so
3455      // we re-check this fully every hour for the rare cases it has changed.
3456      if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
3457           isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
3458          return false;
3459      }
3460  
3461      if (isguestuser($user)) {
3462          return false;
3463      }
3464  
3465      if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3466          return true;
3467      }
3468  
3469      if ($strict) {
3470          if (empty($user->id)) {
3471              // Strict mode can be used with existing accounts only.
3472              return true;
3473          }
3474          if (!profile_has_required_custom_fields_set($user->id)) {
3475              return true;
3476          }
3477          if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3478              $SESSION->fullysetupstrict = time();
3479          }
3480      }
3481  
3482      return false;
3483  }
3484  
3485  /**
3486   * Check whether the user has exceeded the bounce threshold
3487   *
3488   * @param stdClass $user A {@link $USER} object
3489   * @return bool true => User has exceeded bounce threshold
3490   */
3491  function over_bounce_threshold($user) {
3492      global $CFG, $DB;
3493  
3494      if (empty($CFG->handlebounces)) {
3495          return false;
3496      }
3497  
3498      if (empty($user->id)) {
3499          // No real (DB) user, nothing to do here.
3500          return false;
3501      }
3502  
3503      // Set sensible defaults.
3504      if (empty($CFG->minbounces)) {
3505          $CFG->minbounces = 10;
3506      }
3507      if (empty($CFG->bounceratio)) {
3508          $CFG->bounceratio = .20;
3509      }
3510      $bouncecount = 0;
3511      $sendcount = 0;
3512      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3513          $bouncecount = $bounce->value;
3514      }
3515      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3516          $sendcount = $send->value;
3517      }
3518      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3519  }
3520  
3521  /**
3522   * Used to increment or reset email sent count
3523   *
3524   * @param stdClass $user object containing an id
3525   * @param bool $reset will reset the count to 0
3526   * @return void
3527   */
3528  function set_send_count($user, $reset=false) {
3529      global $DB;
3530  
3531      if (empty($user->id)) {
3532          // No real (DB) user, nothing to do here.
3533          return;
3534      }
3535  
3536      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3537          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3538          $DB->update_record('user_preferences', $pref);
3539      } else if (!empty($reset)) {
3540          // If it's not there and we're resetting, don't bother. Make a new one.
3541          $pref = new stdClass();
3542          $pref->name   = 'email_send_count';
3543          $pref->value  = 1;
3544          $pref->userid = $user->id;
3545          $DB->insert_record('user_preferences', $pref, false);
3546      }
3547  }
3548  
3549  /**
3550   * Increment or reset user's email bounce count
3551   *
3552   * @param stdClass $user object containing an id
3553   * @param bool $reset will reset the count to 0
3554   */
3555  function set_bounce_count($user, $reset=false) {
3556      global $DB;
3557  
3558      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3559          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3560          $DB->update_record('user_preferences', $pref);
3561      } else if (!empty($reset)) {
3562          // If it's not there and we're resetting, don't bother. Make a new one.
3563          $pref = new stdClass();
3564          $pref->name   = 'email_bounce_count';
3565          $pref->value  = 1;
3566          $pref->userid = $user->id;
3567          $DB->insert_record('user_preferences', $pref, false);
3568      }
3569  }
3570  
3571  /**
3572   * Determines if the logged in user is currently moving an activity
3573   *
3574   * @param int $courseid The id of the course being tested
3575   * @return bool
3576   */
3577  function ismoving($courseid) {
3578      global $USER;
3579  
3580      if (!empty($USER->activitycopy)) {
3581          return ($USER->activitycopycourse == $courseid);
3582      }
3583      return false;
3584  }
3585  
3586  /**
3587   * Returns a persons full name
3588   *
3589   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3590   * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3591   * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3592   * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3593   *
3594   * @param stdClass $user A {@link $USER} object to get full name of.
3595   * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3596   * @return string
3597   */
3598  function fullname($user, $override=false) {
3599      global $CFG, $SESSION;
3600  
3601      if (!isset($user->firstname) and !isset($user->lastname)) {
3602          return '';
3603      }
3604  
3605      // Get all of the name fields.
3606      $allnames = \core_user\fields::get_name_fields();
3607      if ($CFG->debugdeveloper) {
3608          foreach ($allnames as $allname) {
3609              if (!property_exists($user, $allname)) {
3610                  // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
3611                  debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3612                  // Message has been sent, no point in sending the message multiple times.
3613                  break;
3614              }
3615          }
3616      }
3617  
3618      if (!$override) {
3619          if (!empty($CFG->forcefirstname)) {
3620              $user->firstname = $CFG->forcefirstname;
3621          }
3622          if (!empty($CFG->forcelastname)) {
3623              $user->lastname = $CFG->forcelastname;
3624          }
3625      }
3626  
3627      if (!empty($SESSION->fullnamedisplay)) {
3628          $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3629      }
3630  
3631      $template = null;
3632      // If the fullnamedisplay setting is available, set the template to that.
3633      if (isset($CFG->fullnamedisplay)) {
3634          $template = $CFG->fullnamedisplay;
3635      }
3636      // If the template is empty, or set to language, return the language string.
3637      if ((empty($template) || $template == 'language') && !$override) {
3638          return get_string('fullnamedisplay', null, $user);
3639      }
3640  
3641      // Check to see if we are displaying according to the alternative full name format.
3642      if ($override) {
3643          if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
3644              // Default to show just the user names according to the fullnamedisplay string.
3645              return get_string('fullnamedisplay', null, $user);
3646          } else {
3647              // If the override is true, then change the template to use the complete name.
3648              $template = $CFG->alternativefullnameformat;
3649          }
3650      }
3651  
3652      $requirednames = array();
3653      // With each name, see if it is in the display name template, and add it to the required names array if it is.
3654      foreach ($allnames as $allname) {
3655          if (strpos($template, $allname) !== false) {
3656              $requirednames[] = $allname;
3657          }
3658      }
3659  
3660      $displayname = $template;
3661      // Switch in the actual data into the template.
3662      foreach ($requirednames as $altname) {
3663          if (isset($user->$altname)) {
3664              // Using empty() on the below if statement causes breakages.
3665              if ((string)$user->$altname == '') {
3666                  $displayname = str_replace($altname, 'EMPTY', $displayname);
3667              } else {
3668                  $displayname = str_replace($altname, $user->$altname, $displayname);
3669              }
3670          } else {
3671              $displayname = str_replace($altname, 'EMPTY', $displayname);
3672          }
3673      }
3674      // Tidy up any misc. characters (Not perfect, but gets most characters).
3675      // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3676      // katakana and parenthesis.
3677      $patterns = array();
3678      // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3679      // filled in by a user.
3680      // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3681      $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3682      // This regular expression is to remove any double spaces in the display name.
3683      $patterns[] = '/\s{2,}/u';
3684      foreach ($patterns as $pattern) {
3685          $displayname = preg_replace($pattern, ' ', $displayname);
3686      }
3687  
3688      // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3689      $displayname = trim($displayname);
3690      if (empty($displayname)) {
3691          // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3692          // people in general feel is a good setting to fall back on.
3693          $displayname = $user->firstname;
3694      }
3695      return $displayname;
3696  }
3697  
3698  /**
3699   * Reduces lines of duplicated code for getting user name fields.
3700   *
3701   * See also {@link user_picture::unalias()}
3702   *
3703   * @param object $addtoobject Object to add user name fields to.
3704   * @param object $secondobject Object that contains user name field information.
3705   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3706   * @param array $additionalfields Additional fields to be matched with data in the second object.
3707   * The key can be set to the user table field name.
3708   * @return object User name fields.
3709   */
3710  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3711      $fields = [];
3712      foreach (\core_user\fields::get_name_fields() as $field) {
3713          $fields[$field] = $prefix . $field;
3714      }
3715      if ($additionalfields) {
3716          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3717          // the key is a number and then sets the key to the array value.
3718          foreach ($additionalfields as $key => $value) {
3719              if (is_numeric($key)) {
3720                  $additionalfields[$value] = $prefix . $value;
3721                  unset($additionalfields[$key]);
3722              } else {
3723                  $additionalfields[$key] = $prefix . $value;
3724              }
3725          }
3726          $fields = array_merge($fields, $additionalfields);
3727      }
3728      foreach ($fields as $key => $field) {
3729          // Important that we have all of the user name fields present in the object that we are sending back.
3730          $addtoobject->$key = '';
3731          if (isset($secondobject->$field)) {
3732              $addtoobject->$key = $secondobject->$field;
3733          }
3734      }
3735      return $addtoobject;
3736  }
3737  
3738  /**
3739   * Returns an array of values in order of occurance in a provided string.
3740   * The key in the result is the character postion in the string.
3741   *
3742   * @param array $values Values to be found in the string format
3743   * @param string $stringformat The string which may contain values being searched for.
3744   * @return array An array of values in order according to placement in the string format.
3745   */
3746  function order_in_string($values, $stringformat) {
3747      $valuearray = array();
3748      foreach ($values as $value) {
3749          $pattern = "/$value\b/";
3750          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3751          if (preg_match($pattern, $stringformat)) {
3752              $replacement = "thing";
3753              // Replace the value with something more unique to ensure we get the right position when using strpos().
3754              $newformat = preg_replace($pattern, $replacement, $stringformat);
3755              $position = strpos($newformat, $replacement);
3756              $valuearray[$position] = $value;
3757          }
3758      }
3759      ksort($valuearray);
3760      return $valuearray;
3761  }
3762  
3763  /**
3764   * Returns whether a given authentication plugin exists.
3765   *
3766   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3767   * @return boolean Whether the plugin is available.
3768   */
3769  function exists_auth_plugin($auth) {
3770      global $CFG;
3771  
3772      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3773          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3774      }
3775      return false;
3776  }
3777  
3778  /**
3779   * Checks if a given plugin is in the list of enabled authentication plugins.
3780   *
3781   * @param string $auth Authentication plugin.
3782   * @return boolean Whether the plugin is enabled.
3783   */
3784  function is_enabled_auth($auth) {
3785      if (empty($auth)) {
3786          return false;
3787      }
3788  
3789      $enabled = get_enabled_auth_plugins();
3790  
3791      return in_array($auth, $enabled);
3792  }
3793  
3794  /**
3795   * Returns an authentication plugin instance.
3796   *
3797   * @param string $auth name of authentication plugin
3798   * @return auth_plugin_base An instance of the required authentication plugin.
3799   */
3800  function get_auth_plugin($auth) {
3801      global $CFG;
3802  
3803      // Check the plugin exists first.
3804      if (! exists_auth_plugin($auth)) {
3805          throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3806      }
3807  
3808      // Return auth plugin instance.
3809      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3810      $class = "auth_plugin_$auth";
3811      return new $class;
3812  }
3813  
3814  /**
3815   * Returns array of active auth plugins.
3816   *
3817   * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3818   * @return array
3819   */
3820  function get_enabled_auth_plugins($fix=false) {
3821      global $CFG;
3822  
3823      $default = array('manual', 'nologin');
3824  
3825      if (empty($CFG->auth)) {
3826          $auths = array();
3827      } else {
3828          $auths = explode(',', $CFG->auth);
3829      }
3830  
3831      $auths = array_unique($auths);
3832      $oldauthconfig = implode(',', $auths);
3833      foreach ($auths as $k => $authname) {
3834          if (in_array($authname, $default)) {
3835              // The manual and nologin plugin never need to be stored.
3836              unset($auths[$k]);
3837          } else if (!exists_auth_plugin($authname)) {
3838              debugging(get_string('authpluginnotfound', 'debug', $authname));
3839              unset($auths[$k]);
3840          }
3841      }
3842  
3843      // Ideally only explicit interaction from a human admin should trigger a
3844      // change in auth config, see MDL-70424 for details.
3845      if ($fix) {
3846          $newconfig = implode(',', $auths);
3847          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3848              add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3849              set_config('auth', $newconfig);
3850          }
3851      }
3852  
3853      return (array_merge($default, $auths));
3854  }
3855  
3856  /**
3857   * Returns true if an internal authentication method is being used.
3858   * if method not specified then, global default is assumed
3859   *
3860   * @param string $auth Form of authentication required
3861   * @return bool
3862   */
3863  function is_internal_auth($auth) {
3864      // Throws error if bad $auth.
3865      $authplugin = get_auth_plugin($auth);
3866      return $authplugin->is_internal();
3867  }
3868  
3869  /**
3870   * Returns true if the user is a 'restored' one.
3871   *
3872   * Used in the login process to inform the user and allow him/her to reset the password
3873   *
3874   * @param string $username username to be checked
3875   * @return bool
3876   */
3877  function is_restored_user($username) {
3878      global $CFG, $DB;
3879  
3880      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3881  }
3882  
3883  /**
3884   * Returns an array of user fields
3885   *
3886   * @return array User field/column names
3887   */
3888  function get_user_fieldnames() {
3889      global $DB;
3890  
3891      $fieldarray = $DB->get_columns('user');
3892      unset($fieldarray['id']);
3893      $fieldarray = array_keys($fieldarray);
3894  
3895      return $fieldarray;
3896  }
3897  
3898  /**
3899   * Returns the string of the language for the new user.
3900   *
3901   * @return string language for the new user
3902   */
3903  function get_newuser_language() {
3904      global $CFG, $SESSION;
3905      return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3906  }
3907  
3908  /**
3909   * Creates a bare-bones user record
3910   *
3911   * @todo Outline auth types and provide code example
3912   *
3913   * @param string $username New user's username to add to record
3914   * @param string $password New user's password to add to record
3915   * @param string $auth Form of authentication required
3916   * @return stdClass A complete user object
3917   */
3918  function create_user_record($username, $password, $auth = 'manual') {
3919      global $CFG, $DB, $SESSION;
3920      require_once($CFG->dirroot.'/user/profile/lib.php');
3921      require_once($CFG->dirroot.'/user/lib.php');
3922  
3923      // Just in case check text case.
3924      $username = trim(core_text::strtolower($username));
3925  
3926      $authplugin = get_auth_plugin($auth);
3927      $customfields = $authplugin->get_custom_user_profile_fields();
3928      $newuser = new stdClass();
3929      if ($newinfo = $authplugin->get_userinfo($username)) {
3930          $newinfo = truncate_userinfo($newinfo);
3931          foreach ($newinfo as $key => $value) {
3932              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3933                  $newuser->$key = $value;
3934              }
3935          }
3936      }
3937  
3938      if (!empty($newuser->email)) {
3939          if (email_is_not_allowed($newuser->email)) {
3940              unset($newuser->email);
3941          }
3942      }
3943  
3944      $newuser->auth = $auth;
3945      $newuser->username = $username;
3946  
3947      // Fix for MDL-8480
3948      // user CFG lang for user if $newuser->lang is empty
3949      // or $user->lang is not an installed language.
3950      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3951          $newuser->lang = get_newuser_language();
3952      }
3953      $newuser->confirmed = 1;
3954      $newuser->lastip = getremoteaddr();
3955      $newuser->timecreated = time();
3956      $newuser->timemodified = $newuser->timecreated;
3957      $newuser->mnethostid = $CFG->mnet_localhost_id;
3958  
3959      $newuser->id = user_create_user($newuser, false, false);
3960  
3961      // Save user profile data.
3962      profile_save_data($newuser);
3963  
3964      $user = get_complete_user_data('id', $newuser->id);
3965      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3966          set_user_preference('auth_forcepasswordchange', 1, $user);
3967      }
3968      // Set the password.
3969      update_internal_user_password($user, $password);
3970  
3971      // Trigger event.
3972      \core\event\user_created::create_from_userid($newuser->id)->trigger();
3973  
3974      return $user;
3975  }
3976  
3977  /**
3978   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3979   *
3980   * @param string $username user's username to update the record
3981   * @return stdClass A complete user object
3982   */
3983  function update_user_record($username) {
3984      global $DB, $CFG;
3985      // Just in case check text case.
3986      $username = trim(core_text::strtolower($username));
3987  
3988      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3989      return update_user_record_by_id($oldinfo->id);
3990  }
3991  
3992  /**
3993   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3994   *
3995   * @param int $id user id
3996   * @return stdClass A complete user object
3997   */
3998  function update_user_record_by_id($id) {
3999      global $DB, $CFG;
4000      require_once($CFG->dirroot."/user/profile/lib.php");
4001      require_once($CFG->dirroot.'/user/lib.php');
4002  
4003      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
4004      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
4005  
4006      $newuser = array();
4007      $userauth = get_auth_plugin($oldinfo->auth);
4008  
4009      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
4010          $newinfo = truncate_userinfo($newinfo);
4011          $customfields = $userauth->get_custom_user_profile_fields();
4012  
4013          foreach ($newinfo as $key => $value) {
4014              $iscustom = in_array($key, $customfields);
4015              if (!$iscustom) {
4016                  $key = strtolower($key);
4017              }
4018              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4019                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4020                  // Unknown or must not be changed.
4021                  continue;
4022              }
4023              if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
4024                  continue;
4025              }
4026              $confval = $userauth->config->{'field_updatelocal_' . $key};
4027              $lockval = $userauth->config->{'field_lock_' . $key};
4028              if ($confval === 'onlogin') {
4029                  // MDL-4207 Don't overwrite modified user profile values with
4030                  // empty LDAP values when 'unlocked if empty' is set. The purpose
4031                  // of the setting 'unlocked if empty' is to allow the user to fill
4032                  // in a value for the selected field _if LDAP is giving
4033                  // nothing_ for this field. Thus it makes sense to let this value
4034                  // stand in until LDAP is giving a value for this field.
4035                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
4036                      if ($iscustom || (in_array($key, $userauth->userfields) &&
4037                              ((string)$oldinfo->$key !== (string)$value))) {
4038                          $newuser[$key] = (string)$value;
4039                      }
4040                  }
4041              }
4042          }
4043          if ($newuser) {
4044              $newuser['id'] = $oldinfo->id;
4045              $newuser['timemodified'] = time();
4046              user_update_user((object) $newuser, false, false);
4047  
4048              // Save user profile data.
4049              profile_save_data((object) $newuser);
4050  
4051              // Trigger event.
4052              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
4053          }
4054      }
4055  
4056      return get_complete_user_data('id', $oldinfo->id);
4057  }
4058  
4059  /**
4060   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4061   *
4062   * @param array $info Array of user properties to truncate if needed
4063   * @return array The now truncated information that was passed in
4064   */
4065  function truncate_userinfo(array $info) {
4066      // Define the limits.
4067      $limit = array(
4068          'username'    => 100,
4069          'idnumber'    => 255,
4070          'firstname'   => 100,
4071          'lastname'    => 100,
4072          'email'       => 100,
4073          'phone1'      =>  20,
4074          'phone2'      =>  20,
4075          'institution' => 255,
4076          'department'  => 255,
4077          'address'     => 255,
4078          'city'        => 120,
4079          'country'     =>   2,
4080      );
4081  
4082      // Apply where needed.
4083      foreach (array_keys($info) as $key) {
4084          if (!empty($limit[$key])) {
4085              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4086          }
4087      }
4088  
4089      return $info;
4090  }
4091  
4092  /**
4093   * Marks user deleted in internal user database and notifies the auth plugin.
4094   * Also unenrols user from all roles and does other cleanup.
4095   *
4096   * Any plugin that needs to purge user data should register the 'user_deleted' event.
4097   *
4098   * @param stdClass $user full user object before delete
4099   * @return boolean success
4100   * @throws coding_exception if invalid $user parameter detected
4101   */
4102  function delete_user(stdClass $user) {
4103      global $CFG, $DB, $SESSION;
4104      require_once($CFG->libdir.'/grouplib.php');
4105      require_once($CFG->libdir.'/gradelib.php');
4106      require_once($CFG->dirroot.'/message/lib.php');
4107      require_once($CFG->dirroot.'/user/lib.php');
4108  
4109      // Make sure nobody sends bogus record type as parameter.
4110      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4111          throw new coding_exception('Invalid $user parameter in delete_user() detected');
4112      }
4113  
4114      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4115      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4116          debugging('Attempt to delete unknown user account.');
4117          return false;
4118      }
4119  
4120      // There must be always exactly one guest record, originally the guest account was identified by username only,
4121      // now we use $CFG->siteguest for performance reasons.
4122      if ($user->username === 'guest' or isguestuser($user)) {
4123          debugging('Guest user account can not be deleted.');
4124          return false;
4125      }
4126  
4127      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4128      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4129      if ($user->auth === 'manual' and is_siteadmin($user)) {
4130          debugging('Local administrator accounts can not be deleted.');
4131          return false;
4132      }
4133  
4134      // Allow plugins to use this user object before we completely delete it.
4135      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4136          foreach ($pluginsfunction as $plugintype => $plugins) {
4137              foreach ($plugins as $pluginfunction) {
4138                  $pluginfunction($user);
4139              }
4140          }
4141      }
4142  
4143      // Keep user record before updating it, as we have to pass this to user_deleted event.
4144      $olduser = clone $user;
4145  
4146      // Keep a copy of user context, we need it for event.
4147      $usercontext = context_user::instance($user->id);
4148  
4149      // Delete all grades - backup is kept in grade_grades_history table.
4150      grade_user_delete($user->id);
4151  
4152      // TODO: remove from cohorts using standard API here.
4153  
4154      // Remove user tags.
4155      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4156  
4157      // Unconditionally unenrol from all courses.
4158      enrol_user_delete($user);
4159  
4160      // Unenrol from all roles in all contexts.
4161      // This might be slow but it is really needed - modules might do some extra cleanup!
4162      role_unassign_all(array('userid' => $user->id));
4163  
4164      // Notify the competency subsystem.
4165      \core_competency\api::hook_user_deleted($user->id);
4166  
4167      // Now do a brute force cleanup.
4168  
4169      // Delete all user events and subscription events.
4170      $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4171  
4172      // Now, delete all calendar subscription from the user.
4173      $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4174  
4175      // Remove from all cohorts.
4176      $DB->delete_records('cohort_members', array('userid' => $user->id));
4177  
4178      // Remove from all groups.
4179      $DB->delete_records('groups_members', array('userid' => $user->id));
4180  
4181      // Brute force unenrol from all courses.
4182      $DB->delete_records('user_enrolments', array('userid' => $user->id));
4183  
4184      // Purge user preferences.
4185      $DB->delete_records('user_preferences', array('userid' => $user->id));
4186  
4187      // Purge user extra profile info.
4188      $DB->delete_records('user_info_data', array('userid' => $user->id));
4189  
4190      // Purge log of previous password hashes.
4191      $DB->delete_records('user_password_history', array('userid' => $user->id));
4192  
4193      // Last course access not necessary either.
4194      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4195      // Remove all user tokens.
4196      $DB->delete_records('external_tokens', array('userid' => $user->id));
4197  
4198      // Unauthorise the user for all services.
4199      $DB->delete_records('external_services_users', array('userid' => $user->id));
4200  
4201      // Remove users private keys.
4202      $DB->delete_records('user_private_key', array('userid' => $user->id));
4203  
4204      // Remove users customised pages.
4205      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4206  
4207      // Remove user's oauth2 refresh tokens, if present.
4208      $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
4209  
4210      // Delete user from $SESSION->bulk_users.
4211      if (isset($SESSION->bulk_users[$user->id])) {
4212          unset($SESSION->bulk_users[$user->id]);
4213      }
4214  
4215      // Force logout - may fail if file based sessions used, sorry.
4216      \core\session\manager::kill_user_sessions($user->id);
4217  
4218      // Generate username from email address, or a fake email.
4219      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4220  
4221      $deltime = time();
4222      $deltimelength = core_text::strlen((string) $deltime);
4223  
4224      // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4225      $delname = clean_param($delemail, PARAM_USERNAME);
4226      $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4227  
4228      // Workaround for bulk deletes of users with the same email address.
4229      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4230          $delname++;
4231      }
4232  
4233      // Mark internal user record as "deleted".
4234      $updateuser = new stdClass();
4235      $updateuser->id           = $user->id;
4236      $updateuser->deleted      = 1;
4237      $updateuser->username     = $delname;            // Remember it just in case.
4238      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4239      $updateuser->idnumber     = '';                  // Clear this field to free it up.
4240      $updateuser->picture      = 0;
4241      $updateuser->timemodified = $deltime;
4242  
4243      // Don't trigger update event, as user is being deleted.
4244      user_update_user($updateuser, false, false);
4245  
4246      // Delete all content associated with the user context, but not the context itself.
4247      $usercontext->delete_content();
4248  
4249      // Delete any search data.
4250      \core_search\manager::context_deleted($usercontext);
4251  
4252      // Any plugin that needs to cleanup should register this event.
4253      // Trigger event.
4254      $event = \core\event\user_deleted::create(
4255              array(
4256                  'objectid' => $user->id,
4257                  'relateduserid' => $user->id,
4258                  'context' => $usercontext,
4259                  'other' => array(
4260                      'username' => $user->username,
4261                      'email' => $user->email,
4262                      'idnumber' => $user->idnumber,
4263                      'picture' => $user->picture,
4264                      'mnethostid' => $user->mnethostid
4265                      )
4266                  )
4267              );
4268      $event->add_record_snapshot('user', $olduser);
4269      $event->trigger();
4270  
4271      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4272      // should know about this updated property persisted to the user's table.
4273      $user->timemodified = $updateuser->timemodified;
4274  
4275      // Notify auth plugin - do not block the delete even when plugin fails.
4276      $authplugin = get_auth_plugin($user->auth);
4277      $authplugin->user_delete($user);
4278  
4279      return true;
4280  }
4281  
4282  /**
4283   * Retrieve the guest user object.
4284   *
4285   * @return stdClass A {@link $USER} object
4286   */
4287  function guest_user() {
4288      global $CFG, $DB;
4289  
4290      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4291          $newuser->confirmed = 1;
4292          $newuser->lang = get_newuser_language();
4293          $newuser->lastip = getremoteaddr();
4294      }
4295  
4296      return $newuser;
4297  }
4298  
4299  /**
4300   * Authenticates a user against the chosen authentication mechanism
4301   *
4302   * Given a username and password, this function looks them
4303   * up using the currently selected authentication mechanism,
4304   * and if the authentication is successful, it returns a
4305   * valid $user object from the 'user' table.
4306   *
4307   * Uses auth_ functions from the currently active auth module
4308   *
4309   * After authenticate_user_login() returns success, you will need to
4310   * log that the user has logged in, and call complete_user_login() to set
4311   * the session up.
4312   *
4313   * Note: this function works only with non-mnet accounts!
4314   *
4315   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4316   * @param string $password  User's password
4317   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4318   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4319   * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
4320   * @return stdClass|false A {@link $USER} object or false if error
4321   */
4322  function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
4323      global $CFG, $DB, $PAGE;
4324      require_once("$CFG->libdir/authlib.php");
4325  
4326      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4327          // we have found the user
4328  
4329      } else if (!empty($CFG->authloginviaemail)) {
4330          if ($email = clean_param($username, PARAM_EMAIL)) {
4331              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4332              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4333              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4334              if (count($users) === 1) {
4335                  // Use email for login only if unique.
4336                  $user = reset($users);
4337                  $user = get_complete_user_data('id', $user->id);
4338                  $username = $user->username;
4339              }
4340              unset($users);
4341          }
4342      }
4343  
4344      // Make sure this request came from the login form.
4345      if (!\core\session\manager::validate_login_token($logintoken)) {
4346          $failurereason = AUTH_LOGIN_FAILED;
4347  
4348          // Trigger login failed event (specifying the ID of the found user, if available).
4349          \core\event\user_login_failed::create([
4350              'userid' => ($user->id ?? 0),
4351              'other' => [
4352                  'username' => $username,
4353                  'reason' => $failurereason,
4354              ],
4355          ])->trigger();
4356  
4357          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4358          return false;
4359      }
4360  
4361      $authsenabled = get_enabled_auth_plugins();
4362  
4363      if ($user) {
4364          // Use manual if auth not set.
4365          $auth = empty($user->auth) ? 'manual' : $user->auth;
4366  
4367          if (in_array($user->auth, $authsenabled)) {
4368              $authplugin = get_auth_plugin($user->auth);
4369              $authplugin->pre_user_login_hook($user);
4370          }
4371  
4372          if (!empty($user->suspended)) {
4373              $failurereason = AUTH_LOGIN_SUSPENDED;
4374  
4375              // Trigger login failed event.
4376              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4377                      'other' => array('username' => $username, 'reason' => $failurereason)));
4378              $event->trigger();
4379              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4380              return false;
4381          }
4382          if ($auth=='nologin' or !is_enabled_auth($auth)) {
4383              // Legacy way to suspend user.
4384              $failurereason = AUTH_LOGIN_SUSPENDED;
4385  
4386              // Trigger login failed event.
4387              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4388                      'other' => array('username' => $username, 'reason' => $failurereason)));
4389              $event->trigger();
4390              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4391              return false;
4392          }
4393          $auths = array($auth);
4394  
4395      } else {
4396          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4397          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4398              $failurereason = AUTH_LOGIN_NOUSER;
4399  
4400              // Trigger login failed event.
4401              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4402                      'reason' => $failurereason)));
4403              $event->trigger();
4404              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4405              return false;
4406          }
4407  
4408          // User does not exist.
4409          $auths = $authsenabled;
4410          $user = new stdClass();
4411          $user->id = 0;
4412      }
4413  
4414      if ($ignorelockout) {
4415          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4416          // or this function is called from a SSO script.
4417      } else if ($user->id) {
4418          // Verify login lockout after other ways that may prevent user login.
4419          if (login_is_lockedout($user)) {
4420              $failurereason = AUTH_LOGIN_LOCKOUT;
4421  
4422              // Trigger login failed event.
4423              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4424                      'other' => array('username' => $username, 'reason' => $failurereason)));
4425              $event->trigger();
4426  
4427              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4428              return false;
4429          }
4430      } else {
4431          // We can not lockout non-existing accounts.
4432      }
4433  
4434      foreach ($auths as $auth) {
4435          $authplugin = get_auth_plugin($auth);
4436  
4437          // On auth fail fall through to the next plugin.
4438          if (!$authplugin->user_login($username, $password)) {
4439              continue;
4440          }
4441  
4442          // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4443          if (!empty($CFG->passwordpolicycheckonlogin)) {
4444              $errmsg = '';
4445              $passed = check_password_policy($password, $errmsg, $user);
4446              if (!$passed) {
4447                  // First trigger event for failure.
4448                  $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4449                  $failedevent->trigger();
4450  
4451                  // If able to change password, set flag and move on.
4452                  if ($authplugin->can_change_password()) {
4453                      // Check if we are on internal change password page, or service is external, don't show notification.
4454                      $internalchangeurl = new moodle_url('/login/change_password.php');
4455                      if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4456                          \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4457                      }
4458                      set_user_preference('auth_forcepasswordchange', 1, $user);
4459                  } else if ($authplugin->can_reset_password()) {
4460                      // Else force a reset if possible.
4461                      \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4462                      redirect(new moodle_url('/login/forgot_password.php'));
4463                  } else {
4464                      $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4465                      // If support page is set, add link for help.
4466                      if (!empty($CFG->supportpage)) {
4467                          $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4468                          $link = \html_writer::tag('p', $link);
4469                          $notifymsg .= $link;
4470                      }
4471  
4472                      // If no change or reset is possible, add a notification for user.
4473                      \core\notification::error($notifymsg);
4474                  }
4475              }
4476          }
4477  
4478          // Successful authentication.
4479          if ($user->id) {
4480              // User already exists in database.
4481              if (empty($user->auth)) {
4482                  // For some reason auth isn't set yet.
4483                  $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4484                  $user->auth = $auth;
4485              }
4486  
4487              // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4488              // the current hash algorithm while we have access to the user's password.
4489              update_internal_user_password($user, $password);
4490  
4491              if ($authplugin->is_synchronised_with_external()) {
4492                  // Update user record from external DB.
4493                  $user = update_user_record_by_id($user->id);
4494              }
4495          } else {
4496              // The user is authenticated but user creation may be disabled.
4497              if (!empty($CFG->authpreventaccountcreation)) {
4498                  $failurereason = AUTH_LOGIN_UNAUTHORISED;
4499  
4500                  // Trigger login failed event.
4501                  $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4502                          'reason' => $failurereason)));
4503                  $event->trigger();
4504  
4505                  error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4506                          $_SERVER['HTTP_USER_AGENT']);
4507                  return false;
4508              } else {
4509                  $user = create_user_record($username, $password, $auth);
4510              }
4511          }
4512  
4513          $authplugin->sync_roles($user);
4514  
4515          foreach ($authsenabled as $hau) {
4516              $hauth = get_auth_plugin($hau);
4517              $hauth->user_authenticated_hook($user, $username, $password);
4518          }
4519  
4520          if (empty($user->id)) {
4521              $failurereason = AUTH_LOGIN_NOUSER;
4522              // Trigger login failed event.
4523              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4524                      'reason' => $failurereason)));
4525              $event->trigger();
4526              return false;
4527          }
4528  
4529          if (!empty($user->suspended)) {
4530              // Just in case some auth plugin suspended account.
4531              $failurereason = AUTH_LOGIN_SUSPENDED;
4532              // Trigger login failed event.
4533              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4534                      'other' => array('username' => $username, 'reason' => $failurereason)));
4535              $event->trigger();
4536              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4537              return false;
4538          }
4539  
4540          login_attempt_valid($user);
4541          $failurereason = AUTH_LOGIN_OK;
4542          return $user;
4543      }
4544  
4545      // Failed if all the plugins have failed.
4546      if (debugging('', DEBUG_ALL)) {
4547          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4548      }
4549  
4550      if ($user->id) {
4551          login_attempt_failed($user);
4552          $failurereason = AUTH_LOGIN_FAILED;
4553          // Trigger login failed event.
4554          $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4555                  'other' => array('username' => $username, 'reason' => $failurereason)));
4556          $event->trigger();
4557      } else {
4558          $failurereason = AUTH_LOGIN_NOUSER;
4559          // Trigger login failed event.
4560          $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4561                  'reason' => $failurereason)));
4562          $event->trigger();
4563      }
4564  
4565      return false;
4566  }
4567  
4568  /**
4569   * Call to complete the user login process after authenticate_user_login()
4570   * has succeeded. It will setup the $USER variable and other required bits
4571   * and pieces.
4572   *
4573   * NOTE:
4574   * - It will NOT log anything -- up to the caller to decide what to log.
4575   * - this function does not set any cookies any more!
4576   *
4577   * @param stdClass $user
4578   * @param array $extrauserinfo
4579   * @return stdClass A {@link $USER} object - BC only, do not use
4580   */
4581  function complete_user_login($user, array $extrauserinfo = []) {
4582      global $CFG, $DB, $USER, $SESSION;
4583  
4584      \core\session\manager::login_user($user);
4585  
4586      // Reload preferences from DB.
4587      unset($USER->preference);
4588      check_user_preferences_loaded($USER);
4589  
4590      // Update login times.
4591      update_user_login_times();
4592  
4593      // Extra session prefs init.
4594      set_login_session_preferences();
4595  
4596      // Trigger login event.
4597      $event = \core\event\user_loggedin::create(
4598          array(
4599              'userid' => $USER->id,
4600              'objectid' => $USER->id,
4601              'other' => [
4602                  'username' => $USER->username,
4603                  'extrauserinfo' => $extrauserinfo
4604              ]
4605          )
4606      );
4607      $event->trigger();
4608  
4609      // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4610      // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4611      // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4612      $loginip = getremoteaddr();
4613      $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4614      $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4615  
4616      if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4617  
4618          $logintime = time();
4619          $ismoodleapp = false;
4620          $useragent = \core_useragent::get_user_agent_string();
4621  
4622          // Schedule adhoc task to sent a login notification to the user.
4623          $task = new \core\task\send_login_notifications();
4624          $task->set_userid($USER->id);
4625          $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4626          $task->set_component('core');
4627          \core\task\manager::queue_adhoc_task($task);
4628      }
4629  
4630      // Queue migrating the messaging data, if we need to.
4631      if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4632          // Check if there are any legacy messages to migrate.
4633          if (\core_message\helper::legacy_messages_exist($USER->id)) {
4634              \core_message\task\migrate_message_data::queue_task($USER->id);
4635          } else {
4636              set_user_preference('core_message_migrate_data', true, $USER->id);
4637          }
4638      }
4639  
4640      if (isguestuser()) {
4641          // No need to continue when user is THE guest.
4642          return $USER;
4643      }
4644  
4645      if (CLI_SCRIPT) {
4646          // We can redirect to password change URL only in browser.
4647          return $USER;
4648      }
4649  
4650      // Select password change url.
4651      $userauth = get_auth_plugin($USER->auth);
4652  
4653      // Check whether the user should be changing password.
4654      if (get_user_preferences('auth_forcepasswordchange', false)) {
4655          if ($userauth->can_change_password()) {
4656              if ($changeurl = $userauth->change_password_url()) {
4657                  redirect($changeurl);
4658              } else {
4659                  require_once($CFG->dirroot . '/login/lib.php');
4660                  $SESSION->wantsurl = core_login_get_return_url();
4661                  redirect($CFG->wwwroot.'/login/change_password.php');
4662              }
4663          } else {
4664              throw new \moodle_exception('nopasswordchangeforced', 'auth');
4665          }
4666      }
4667      return $USER;
4668  }
4669  
4670  /**
4671   * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4672   *
4673   * @param string $password String to check.
4674   * @return boolean True if the $password matches the format of an md5 sum.
4675   */
4676  function password_is_legacy_hash($password) {
4677      return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4678  }
4679  
4680  /**
4681   * Compare password against hash stored in user object to determine if it is valid.
4682   *
4683   * If necessary it also updates the stored hash to the current format.
4684   *
4685   * @param stdClass $user (Password property may be updated).
4686   * @param string $password Plain text password.
4687   * @return bool True if password is valid.
4688   */
4689  function validate_internal_user_password($user, $password) {
4690      global $CFG;
4691  
4692      if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4693          // Internal password is not used at all, it can not validate.
4694          return false;
4695      }
4696  
4697      // If hash isn't a legacy (md5) hash, validate using the library function.
4698      if (!password_is_legacy_hash($user->password)) {
4699          return password_verify($password, $user->password);
4700      }
4701  
4702      // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4703      // is valid we can then update it to the new algorithm.
4704  
4705      $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4706      $validated = false;
4707  
4708      if ($user->password === md5($password.$sitesalt)
4709              or $user->password === md5($password)
4710              or $user->password === md5(addslashes($password).$sitesalt)
4711              or $user->password === md5(addslashes($password))) {
4712          // Note: we are intentionally using the addslashes() here because we
4713          //       need to accept old password hashes of passwords with magic quotes.
4714          $validated = true;
4715  
4716      } else {
4717          for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4718              $alt = 'passwordsaltalt'.$i;
4719              if (!empty($CFG->$alt)) {
4720                  if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4721                      $validated = true;
4722                      break;
4723                  }
4724              }
4725          }
4726      }
4727  
4728      if ($validated) {
4729          // If the password matches the existing md5 hash, update to the
4730          // current hash algorithm while we have access to the user's password.
4731          update_internal_user_password($user, $password);
4732      }
4733  
4734      return $validated;
4735  }
4736  
4737  /**
4738   * Calculate hash for a plain text password.
4739   *
4740   * @param string $password Plain text password to be hashed.
4741   * @param bool $fasthash If true, use a low cost factor when generating the hash
4742   *                       This is much faster to generate but makes the hash
4743   *                       less secure. It is used when lots of hashes need to
4744   *                       be generated quickly.
4745   * @return string The hashed password.
4746   *
4747   * @throws moodle_exception If a problem occurs while generating the hash.
4748   */
4749  function hash_internal_user_password($password, $fasthash = false) {
4750      global $CFG;
4751  
4752      // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4753      $options = ($fasthash) ? array('cost' => 4) : array();
4754  
4755      $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4756  
4757      if ($generatedhash === false || $generatedhash === null) {
4758          throw new moodle_exception('Failed to generate password hash.');
4759      }
4760  
4761      return $generatedhash;
4762  }
4763  
4764  /**
4765   * Update password hash in user object (if necessary).
4766   *
4767   * The password is updated if:
4768   * 1. The password has changed (the hash of $user->password is different
4769   *    to the hash of $password).
4770   * 2. The existing hash is using an out-of-date algorithm (or the legacy
4771   *    md5 algorithm).
4772   *
4773   * Updating the password will modify the $user object and the database
4774   * record to use the current hashing algorithm.
4775   * It will remove Web Services user tokens too.
4776   *
4777   * @param stdClass $user User object (password property may be updated).
4778   * @param string $password Plain text password.
4779   * @param bool $fasthash If true, use a low cost factor when generating the hash
4780   *                       This is much faster to generate but makes the hash
4781   *                       less secure. It is used when lots of hashes need to
4782   *                       be generated quickly.
4783   * @return bool Always returns true.
4784   */
4785  function update_internal_user_password($user, $password, $fasthash = false) {
4786      global $CFG, $DB;
4787  
4788      // Figure out what the hashed password should be.
4789      if (!isset($user->auth)) {
4790          debugging('User record in update_internal_user_password() must include field auth',
4791                  DEBUG_DEVELOPER);
4792          $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4793      }
4794      $authplugin = get_auth_plugin($user->auth);
4795      if ($authplugin->prevent_local_passwords()) {
4796          $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4797      } else {
4798          $hashedpassword = hash_internal_user_password($password, $fasthash);
4799      }
4800  
4801      $algorithmchanged = false;
4802  
4803      if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4804          // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4805          $passwordchanged = ($user->password !== $hashedpassword);
4806  
4807      } else if (isset($user->password)) {
4808          // If verification fails then it means the password has changed.
4809          $passwordchanged = !password_verify($password, $user->password);
4810          $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4811      } else {
4812          // While creating new user, password in unset in $user object, to avoid
4813          // saving it with user_create()
4814          $passwordchanged = true;
4815      }
4816  
4817      if ($passwordchanged || $algorithmchanged) {
4818          $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4819          $user->password = $hashedpassword;
4820  
4821          // Trigger event.
4822          $user = $DB->get_record('user', array('id' => $user->id));
4823          \core\event\user_password_updated::create_from_user($user)->trigger();
4824  
4825          // Remove WS user tokens.
4826          if (!empty($CFG->passwordchangetokendeletion)) {
4827              require_once($CFG->dirroot.'/webservice/lib.php');
4828              webservice::delete_user_ws_tokens($user->id);
4829          }
4830      }
4831  
4832      return true;
4833  }
4834  
4835  /**
4836   * Get a complete user record, which includes all the info in the user record.
4837   *
4838   * Intended for setting as $USER session variable
4839   *
4840   * @param string $field The user field to be checked for a given value.
4841   * @param string $value The value to match for $field.
4842   * @param int $mnethostid
4843   * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4844   *                              found. Otherwise, it will just return false.
4845   * @return mixed False, or A {@link $USER} object.
4846   */
4847  function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4848      global $CFG, $DB;
4849  
4850      if (!$field || !$value) {
4851          return false;
4852      }
4853  
4854      // Change the field to lowercase.
4855      $field = core_text::strtolower($field);
4856  
4857      // List of case insensitive fields.
4858      $caseinsensitivefields = ['email'];
4859  
4860      // Username input is forced to lowercase and should be case sensitive.
4861      if ($field == 'username') {
4862          $value = core_text::strtolower($value);
4863      }
4864  
4865      // Build the WHERE clause for an SQL query.
4866      $params = array('fieldval' => $value);
4867  
4868      // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4869      // such as MySQL by pre-filtering users with accent-insensitive subselect.
4870      if (in_array($field, $caseinsensitivefields)) {
4871          $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4872          $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4873          $params['fieldval2'] = $value;
4874      } else {
4875          $fieldselect = "$field = :fieldval";
4876          $idsubselect = '';
4877      }
4878      $constraints = "$fieldselect AND deleted <> 1";
4879  
4880      // If we are loading user data based on anything other than id,
4881      // we must also restrict our search based on mnet host.
4882      if ($field != 'id') {
4883          if (empty($mnethostid)) {
4884              // If empty, we restrict to local users.
4885              $mnethostid = $CFG->mnet_localhost_id;
4886          }
4887      }
4888      if (!empty($mnethostid)) {
4889          $params['mnethostid'] = $mnethostid;
4890          $constraints .= " AND mnethostid = :mnethostid";
4891      }
4892  
4893      if ($idsubselect) {
4894          $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4895      }
4896  
4897      // Get all the basic user data.
4898      try {
4899          // Make sure that there's only a single record that matches our query.
4900          // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4901          // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4902          $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4903      } catch (dml_exception $exception) {
4904          if ($throwexception) {
4905              throw $exception;
4906          } else {
4907              // Return false when no records or multiple records were found.
4908              return false;
4909          }
4910      }
4911  
4912      // Get various settings and preferences.
4913  
4914      // Preload preference cache.
4915      check_user_preferences_loaded($user);
4916  
4917      // Load course enrolment related stuff.
4918      $user->lastcourseaccess    = array(); // During last session.
4919      $user->currentcourseaccess = array(); // During current session.
4920      if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4921          foreach ($lastaccesses as $lastaccess) {
4922              $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4923          }
4924      }
4925  
4926      // Add cohort theme.
4927      if (!empty($CFG->allowcohortthemes)) {
4928          require_once($CFG->dirroot . '/cohort/lib.php');
4929          if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4930              $user->cohorttheme = $cohorttheme;
4931          }
4932      }
4933  
4934      // Add the custom profile fields to the user record.
4935      $user->profile = array();
4936      if (!isguestuser($user)) {
4937          require_once($CFG->dirroot.'/user/profile/lib.php');
4938          profile_load_custom_fields($user);
4939      }
4940  
4941      // Rewrite some variables if necessary.
4942      if (!empty($user->description)) {
4943          // No need to cart all of it around.
4944          $user->description = true;
4945      }
4946      if (isguestuser($user)) {
4947          // Guest language always same as site.
4948          $user->lang = get_newuser_language();
4949          // Name always in current language.
4950          $user->firstname = get_string('guestuser');
4951          $user->lastname = ' ';
4952      }
4953  
4954      return $user;
4955  }
4956  
4957  /**
4958   * Validate a password against the configured password policy
4959   *
4960   * @param string $password the password to be checked against the password policy
4961   * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4962   * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
4963   *
4964   * @return bool true if the password is valid according to the policy. false otherwise.
4965   */
4966  function check_password_policy($password, &$errmsg, $user = null) {
4967      global $CFG;
4968  
4969      if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4970          $errmsg = '';
4971          if (core_text::strlen($password) < $CFG->minpasswordlength) {
4972              $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4973          }
4974          if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4975              $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4976          }
4977          if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4978              $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4979          }
4980          if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4981              $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4982          }
4983          if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4984              $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4985          }
4986          if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4987              $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4988          }
4989  
4990          // Fire any additional password policy functions from plugins.
4991          // Plugin functions should output an error message string or empty string for success.
4992          $pluginsfunction = get_plugins_with_function('check_password_policy');
4993          foreach ($pluginsfunction as $plugintype => $plugins) {
4994              foreach ($plugins as $pluginfunction) {
4995                  $pluginerr = $pluginfunction($password, $user);
4996                  if ($pluginerr) {
4997                      $errmsg .= '<div>'. $pluginerr .'</div>';
4998                  }
4999              }
5000          }
5001      }
5002  
5003      if ($errmsg == '') {
5004          return true;
5005      } else {
5006          return false;
5007      }
5008  }
5009  
5010  
5011  /**
5012   * When logging in, this function is run to set certain preferences for the current SESSION.
5013   */
5014  function set_login_session_preferences() {
5015      global $SESSION;
5016  
5017      $SESSION->justloggedin = true;
5018  
5019      unset($SESSION->lang);
5020      unset($SESSION->forcelang);
5021      unset($SESSION->load_navigation_admin);
5022  }
5023  
5024  
5025  /**
5026   * Delete a course, including all related data from the database, and any associated files.
5027   *
5028   * @param mixed $courseorid The id of the course or course object to delete.
5029   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5030   * @return bool true if all the removals succeeded. false if there were any failures. If this
5031   *             method returns false, some of the removals will probably have succeeded, and others
5032   *             failed, but you have no way of knowing which.
5033   */
5034  function delete_course($courseorid, $showfeedback = true) {
5035      global $DB;
5036  
5037      if (is_object($courseorid)) {
5038          $courseid = $courseorid->id;
5039          $course   = $courseorid;
5040      } else {
5041          $courseid = $courseorid;
5042          if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5043              return false;
5044          }
5045      }
5046      $context = context_course::instance($courseid);
5047  
5048      // Frontpage course can not be deleted!!
5049      if ($courseid == SITEID) {
5050          return false;
5051      }
5052  
5053      // Allow plugins to use this course before we completely delete it.
5054      if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5055          foreach ($pluginsfunction as $plugintype => $plugins) {
5056              foreach ($plugins as $pluginfunction) {
5057                  $pluginfunction($course);
5058              }
5059          }
5060      }
5061  
5062      // Tell the search manager we are about to delete a course. This prevents us sending updates
5063      // for each individual context being deleted.
5064      \core_search\manager::course_deleting_start($courseid);
5065  
5066      $handler = core_course\customfield\course_handler::create();
5067      $handler->delete_instance($courseid);
5068  
5069      // Make the course completely empty.
5070      remove_course_contents($courseid, $showfeedback);
5071  
5072      // Delete the course and related context instance.
5073      context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5074  
5075      $DB->delete_records("course", array("id" => $courseid));
5076      $DB->delete_records("course_format_options", array("courseid" => $courseid));
5077  
5078      // Reset all course related caches here.
5079      core_courseformat\base::reset_course_cache($courseid);
5080  
5081      // Tell search that we have deleted the course so it can delete course data from the index.
5082      \core_search\manager::course_deleting_finish($courseid);
5083  
5084      // Trigger a course deleted event.
5085      $event = \core\event\course_deleted::create(array(
5086          'objectid' => $course->id,
5087          'context' => $context,
5088          'other' => array(
5089              'shortname' => $course->shortname,
5090              'fullname' => $course->fullname,
5091              'idnumber' => $course->idnumber
5092              )
5093      ));
5094      $event->add_record_snapshot('course', $course);
5095      $event->trigger();
5096  
5097      return true;
5098  }
5099  
5100  /**
5101   * Clear a course out completely, deleting all content but don't delete the course itself.
5102   *
5103   * This function does not verify any permissions.
5104   *
5105   * Please note this function also deletes all user enrolments,
5106   * enrolment instances and role assignments by default.
5107   *
5108   * $options:
5109   *  - 'keep_roles_and_enrolments' - false by default
5110   *  - 'keep_groups_and_groupings' - false by default
5111   *
5112   * @param int $courseid The id of the course that is being deleted
5113   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5114   * @param array $options extra options
5115   * @return bool true if all the removals succeeded. false if there were any failures. If this
5116   *             method returns false, some of the removals will probably have succeeded, and others
5117   *             failed, but you have no way of knowing which.
5118   */
5119  function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5120      global $CFG, $DB, $OUTPUT;
5121  
5122      require_once($CFG->libdir.'/badgeslib.php');
5123      require_once($CFG->libdir.'/completionlib.php');
5124      require_once($CFG->libdir.'/questionlib.php');
5125      require_once($CFG->libdir.'/gradelib.php');
5126      require_once($CFG->dirroot.'/group/lib.php');
5127      require_once($CFG->dirroot.'/comment/lib.php');
5128      require_once($CFG->dirroot.'/rating/lib.php');
5129      require_once($CFG->dirroot.'/notes/lib.php');
5130  
5131      // Handle course badges.
5132      badges_handle_course_deletion($courseid);
5133  
5134      // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5135      $strdeleted = get_string('deleted').' - ';
5136  
5137      // Some crazy wishlist of stuff we should skip during purging of course content.
5138      $options = (array)$options;
5139  
5140      $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5141      $coursecontext = context_course::instance($courseid);
5142      $fs = get_file_storage();
5143  
5144      // Delete course completion information, this has to be done before grades and enrols.
5145      $cc = new completion_info($course);
5146      $cc->clear_criteria();
5147      if ($showfeedback) {
5148          echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5149      }
5150  
5151      // Remove all data from gradebook - this needs to be done before course modules
5152      // because while deleting this information, the system may need to reference
5153      // the course modules that own the grades.
5154      remove_course_grades($courseid, $showfeedback);
5155      remove_grade_letters($coursecontext, $showfeedback);
5156  
5157      // Delete course blocks in any all child contexts,
5158      // they may depend on modules so delete them first.
5159      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5160      foreach ($childcontexts as $childcontext) {
5161          blocks_delete_all_for_context($childcontext->id);
5162      }
5163      unset($childcontexts);
5164      blocks_delete_all_for_context($coursecontext->id);
5165      if ($showfeedback) {
5166          echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5167      }
5168  
5169      $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5170      rebuild_course_cache($courseid, true);
5171  
5172      // Get the list of all modules that are properly installed.
5173      $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5174  
5175      // Delete every instance of every module,
5176      // this has to be done before deleting of course level stuff.
5177      $locations = core_component::get_plugin_list('mod');
5178      foreach ($locations as $modname => $moddir) {
5179          if ($modname === 'NEWMODULE') {
5180              continue;
5181          }
5182          if (array_key_exists($modname, $allmodules)) {
5183              $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5184                FROM {".$modname."} m
5185                     LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5186               WHERE m.course = :courseid";
5187              $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5188                  'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5189  
5190              include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
5191              $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
5192  
5193              if ($instances) {
5194                  foreach ($instances as $cm) {
5195                      if ($cm->id) {
5196                          // Delete activity context questions and question categories.
5197                          question_delete_activity($cm);
5198                          // Notify the competency subsystem.
5199                          \core_competency\api::hook_course_module_deleted($cm);
5200  
5201                          // Delete all tag instances associated with the instance of this module.
5202                          core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
5203                          core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
5204                      }
5205                      if (function_exists($moddelete)) {
5206                          // This purges all module data in related tables, extra user prefs, settings, etc.
5207                          $moddelete($cm->modinstance);
5208                      } else {
5209                          // NOTE: we should not allow installation of modules with missing delete support!
5210                          debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5211                          $DB->delete_records($modname, array('id' => $cm->modinstance));
5212                      }
5213  
5214                      if ($cm->id) {
5215                          // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5216                          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5217                          $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5218                          $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
5219                          $DB->delete_records('course_modules', array('id' => $cm->id));
5220                          rebuild_course_cache($cm->course, true);
5221                      }
5222                  }
5223              }
5224              if ($instances and $showfeedback) {
5225                  echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5226              }
5227          } else {
5228              // Ooops, this module is not properly installed, force-delete it in the next block.
5229          }
5230      }
5231  
5232      // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5233  
5234      // Delete completion defaults.
5235      $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5236  
5237      // Remove all data from availability and completion tables that is associated
5238      // with course-modules belonging to this course. Note this is done even if the
5239      // features are not enabled now, in case they were enabled previously.
5240      $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5241              'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5242      $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
5243          'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5244  
5245      // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5246      $cms = $DB->get_records('course_modules', array('course' => $course->id));
5247      $allmodulesbyid = array_flip($allmodules);
5248      foreach ($cms as $cm) {
5249          if (array_key_exists($cm->module, $allmodulesbyid)) {
5250              try {
5251                  $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5252              } catch (Exception $e) {
5253                  // Ignore weird or missing table problems.
5254              }
5255          }
5256          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5257          $DB->delete_records('course_modules', array('id' => $cm->id));
5258          rebuild_course_cache($cm->course, true);
5259      }
5260  
5261      if ($showfeedback) {
5262          echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5263      }
5264  
5265      // Delete questions and question categories.
5266      question_delete_course($course);
5267      if ($showfeedback) {
5268          echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5269      }
5270  
5271      // Delete content bank contents.
5272      $cb = new \core_contentbank\contentbank();
5273      $cbdeleted = $cb->delete_contents($coursecontext);
5274      if ($showfeedback && $cbdeleted) {
5275          echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5276      }
5277  
5278      // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5279      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5280      foreach ($childcontexts as $childcontext) {
5281          $childcontext->delete();
5282      }
5283      unset($childcontexts);
5284  
5285      // Remove roles and enrolments by default.
5286      if (empty($options['keep_roles_and_enrolments'])) {
5287          // This hack is used in restore when deleting contents of existing course.
5288          // During restore, we should remove only enrolment related data that the user performing the restore has a
5289          // permission to remove.
5290          $userid = $options['userid'] ?? null;
5291          enrol_course_delete($course, $userid);
5292          role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5293          if ($showfeedback) {
5294              echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5295          }
5296      }
5297  
5298      // Delete any groups, removing members and grouping/course links first.
5299      if (empty($options['keep_groups_and_groupings'])) {
5300          groups_delete_groupings($course->id, $showfeedback);
5301          groups_delete_groups($course->id, $showfeedback);
5302      }
5303  
5304      // Filters be gone!
5305      filter_delete_all_for_context($coursecontext->id);
5306  
5307      // Notes, you shall not pass!
5308      note_delete_all($course->id);
5309  
5310      // Die comments!
5311      comment::delete_comments($coursecontext->id);
5312  
5313      // Ratings are history too.
5314      $delopt = new stdclass();
5315      $delopt->contextid = $coursecontext->id;
5316      $rm = new rating_manager();
5317      $rm->delete_ratings($delopt);
5318  
5319      // Delete course tags.
5320      core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5321  
5322      // Give the course format the opportunity to remove its obscure data.
5323      $format = course_get_format($course);
5324      $format->delete_format_data();
5325  
5326      // Notify the competency subsystem.
5327      \core_competency\api::hook_course_deleted($course);
5328  
5329      // Delete calendar events.
5330      $DB->delete_records('event', array('courseid' => $course->id));
5331      $fs->delete_area_files($coursecontext->id, 'calendar');
5332  
5333      // Delete all related records in other core tables that may have a courseid
5334      // This array stores the tables that need to be cleared, as
5335      // table_name => column_name that contains the course id.
5336      $tablestoclear = array(
5337          'backup_courses' => 'courseid',  // Scheduled backup stuff.
5338          'user_lastaccess' => 'courseid', // User access info.
5339      );
5340      foreach ($tablestoclear as $table => $col) {
5341          $DB->delete_records($table, array($col => $course->id));
5342      }
5343  
5344      // Delete all course backup files.
5345      $fs->delete_area_files($coursecontext->id, 'backup');
5346  
5347      // Cleanup course record - remove links to deleted stuff.
5348      $oldcourse = new stdClass();
5349      $oldcourse->id               = $course->id;
5350      $oldcourse->summary          = '';
5351      $oldcourse->cacherev         = 0;
5352      $oldcourse->legacyfiles      = 0;
5353      if (!empty($options['keep_groups_and_groupings'])) {
5354          $oldcourse->defaultgroupingid = 0;
5355      }
5356      $DB->update_record('course', $oldcourse);
5357  
5358      // Delete course sections.
5359      $DB->delete_records('course_sections', array('course' => $course->id));
5360  
5361      // Delete legacy, section and any other course files.
5362      $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5363  
5364      // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5365      if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5366          // Easy, do not delete the context itself...
5367          $coursecontext->delete_content();
5368      } else {
5369          // Hack alert!!!!
5370          // We can not drop all context stuff because it would bork enrolments and roles,
5371          // there might be also files used by enrol plugins...
5372      }
5373  
5374      // Delete legacy files - just in case some files are still left there after conversion to new file api,
5375      // also some non-standard unsupported plugins may try to store something there.
5376      fulldelete($CFG->dataroot.'/'.$course->id);
5377  
5378      // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5379      course_modinfo::purge_course_cache($courseid);
5380  
5381      // Trigger a course content deleted event.
5382      $event = \core\event\course_content_deleted::create(array(
5383          'objectid' => $course->id,
5384          'context' => $coursecontext,
5385          'other' => array('shortname' => $course->shortname,
5386                           'fullname' => $course->fullname,
5387                           'options' => $options) // Passing this for legacy reasons.
5388      ));
5389      $event->add_record_snapshot('course', $course);
5390      $event->trigger();
5391  
5392      return true;
5393  }
5394  
5395  /**
5396   * Change dates in module - used from course reset.
5397   *
5398   * @param string $modname forum, assignment, etc
5399   * @param array $fields array of date fields from mod table
5400   * @param int $timeshift time difference
5401   * @param int $courseid
5402   * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5403   * @return bool success
5404   */
5405  function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5406      global $CFG, $DB;
5407      include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5408  
5409      $return = true;
5410      $params = array($timeshift, $courseid);
5411      foreach ($fields as $field) {
5412          $updatesql = "UPDATE {".$modname."}
5413                            SET $field = $field + ?
5414                          WHERE course=? AND $field<>0";
5415          if ($modid) {
5416              $updatesql .= ' AND id=?';
5417              $params[] = $modid;
5418          }
5419          $return = $DB->execute($updatesql, $params) && $return;
5420      }
5421  
5422      return $return;
5423  }
5424  
5425  /**
5426   * This function will empty a course of user data.
5427   * It will retain the activities and the structure of the course.
5428   *
5429   * @param object $data an object containing all the settings including courseid (without magic quotes)
5430   * @return array status array of array component, item, error
5431   */
5432  function reset_course_userdata($data) {
5433      global $CFG, $DB;
5434      require_once($CFG->libdir.'/gradelib.php');
5435      require_once($CFG->libdir.'/completionlib.php');
5436      require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5437      require_once($CFG->dirroot.'/group/lib.php');
5438  
5439      $data->courseid = $data->id;
5440      $context = context_course::instance($data->courseid);
5441  
5442      $eventparams = array(
5443          'context' => $context,
5444          'courseid' => $data->id,
5445          'other' => array(
5446              'reset_options' => (array) $data
5447          )
5448      );
5449      $event = \core\event\course_reset_started::create($eventparams);
5450      $event->trigger();
5451  
5452      // Calculate the time shift of dates.
5453      if (!empty($data->reset_start_date)) {
5454          // Time part of course startdate should be zero.
5455          $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5456      } else {
5457          $data->timeshift = 0;
5458      }
5459  
5460      // Result array: component, item, error.
5461      $status = array();
5462  
5463      // Start the resetting.
5464      $componentstr = get_string('general');
5465  
5466      // Move the course start time.
5467      if (!empty($data->reset_start_date) and $data->timeshift) {
5468          // Change course start data.
5469          $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5470          // Update all course and group events - do not move activity events.
5471          $updatesql = "UPDATE {event}
5472                           SET timestart = timestart + ?
5473                         WHERE courseid=? AND instance=0";
5474          $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5475  
5476          // Update any date activity restrictions.
5477          if ($CFG->enableavailability) {
5478              \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5479          }
5480  
5481          // Update completion expected dates.
5482          if ($CFG->enablecompletion) {
5483              $modinfo = get_fast_modinfo($data->courseid);
5484              $changed = false;
5485              foreach ($modinfo->get_cms() as $cm) {
5486                  if ($cm->completion && !empty($cm->completionexpected)) {
5487                      $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5488                          array('id' => $cm->id));
5489                      $changed = true;
5490                  }
5491              }
5492  
5493              // Clear course cache if changes made.
5494              if ($changed) {
5495                  rebuild_course_cache($data->courseid, true);
5496              }
5497  
5498              // Update course date completion criteria.
5499              \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5500          }
5501  
5502          $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5503      }
5504  
5505      if (!empty($data->reset_end_date)) {
5506          // If the user set a end date value respect it.
5507          $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5508      } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5509          // If there is a time shift apply it to the end date as well.
5510          $enddate = $data->reset_end_date_old + $data->timeshift;
5511          $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5512      }
5513  
5514      if (!empty($data->reset_events)) {
5515          $DB->delete_records('event', array('courseid' => $data->courseid));
5516          $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5517      }
5518  
5519      if (!empty($data->reset_notes)) {
5520          require_once($CFG->dirroot.'/notes/lib.php');
5521          note_delete_all($data->courseid);
5522          $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5523      }
5524  
5525      if (!empty($data->delete_blog_associations)) {
5526          require_once($CFG->dirroot.'/blog/lib.php');
5527          blog_remove_associations_for_course($data->courseid);
5528          $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5529      }
5530  
5531      if (!empty($data->reset_completion)) {
5532          // Delete course and activity completion information.
5533          $course = $DB->get_record('course', array('id' => $data->courseid));
5534          $cc = new completion_info($course);
5535          $cc->delete_all_completion_data();
5536          $status[] = array('component' => $componentstr,
5537                  'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5538      }
5539  
5540      if (!empty($data->reset_competency_ratings)) {
5541          \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5542          $status[] = array('component' => $componentstr,
5543              'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5544      }
5545  
5546      $componentstr = get_string('roles');
5547  
5548      if (!empty($data->reset_roles_overrides)) {
5549          $children = $context->get_child_contexts();
5550          foreach ($children as $child) {
5551              $child->delete_capabilities();
5552          }
5553          $context->delete_capabilities();
5554          $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5555      }
5556  
5557      if (!empty($data->reset_roles_local)) {
5558          $children = $context->get_child_contexts();
5559          foreach ($children as $child) {
5560              role_unassign_all(array('contextid' => $child->id));
5561          }
5562          $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5563      }
5564  
5565      // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5566      $data->unenrolled = array();
5567      if (!empty($data->unenrol_users)) {
5568          $plugins = enrol_get_plugins(true);
5569          $instances = enrol_get_instances($data->courseid, true);
5570          foreach ($instances as $key => $instance) {
5571              if (!isset($plugins[$instance->enrol])) {
5572                  unset($instances[$key]);
5573                  continue;
5574              }
5575          }
5576  
5577          $usersroles = enrol_get_course_users_roles($data->courseid);
5578          foreach ($data->unenrol_users as $withroleid) {
5579              if ($withroleid) {
5580                  $sql = "SELECT ue.*
5581                            FROM {user_enrolments} ue
5582                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5583                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5584                            JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5585                  $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5586  
5587              } else {
5588                  // Without any role assigned at course context.
5589                  $sql = "SELECT ue.*
5590                            FROM {user_enrolments} ue
5591                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5592                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5593                       LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5594                           WHERE ra.id IS null";
5595                  $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5596              }
5597  
5598              $rs = $DB->get_recordset_sql($sql, $params);
5599              foreach ($rs as $ue) {
5600                  if (!isset($instances[$ue->enrolid])) {
5601                      continue;
5602                  }
5603                  $instance = $instances[$ue->enrolid];
5604                  $plugin = $plugins[$instance->enrol];
5605                  if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5606                      continue;
5607                  }
5608  
5609                  if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5610                      // If we don't remove all roles and user has more than one role, just remove this role.
5611                      role_unassign($withroleid, $ue->userid, $context->id);
5612  
5613                      unset($usersroles[$ue->userid][$withroleid]);
5614                  } else {
5615                      // If we remove all roles or user has only one role, unenrol user from course.
5616                      $plugin->unenrol_user($instance, $ue->userid);
5617                  }
5618                  $data->unenrolled[$ue->userid] = $ue->userid;
5619              }
5620              $rs->close();
5621          }
5622      }
5623      if (!empty($data->unenrolled)) {
5624          $status[] = array(
5625              'component' => $componentstr,
5626              'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5627              'error' => false
5628          );
5629      }
5630  
5631      $componentstr = get_string('groups');
5632  
5633      // Remove all group members.
5634      if (!empty($data->reset_groups_members)) {
5635          groups_delete_group_members($data->courseid);
5636          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5637      }
5638  
5639      // Remove all groups.
5640      if (!empty($data->reset_groups_remove)) {
5641          groups_delete_groups($data->courseid, false);
5642          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5643      }
5644  
5645      // Remove all grouping members.
5646      if (!empty($data->reset_groupings_members)) {
5647          groups_delete_groupings_groups($data->courseid, false);
5648          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5649      }
5650  
5651      // Remove all groupings.
5652      if (!empty($data->reset_groupings_remove)) {
5653          groups_delete_groupings($data->courseid, false);
5654          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5655      }
5656  
5657      // Look in every instance of every module for data to delete.
5658      $unsupportedmods = array();
5659      if ($allmods = $DB->get_records('modules') ) {
5660          foreach ($allmods as $mod) {
5661              $modname = $mod->name;
5662              $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5663              $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5664              if (file_exists($modfile)) {
5665                  if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5666                      continue; // Skip mods with no instances.
5667                  }
5668                  include_once($modfile);
5669                  if (function_exists($moddeleteuserdata)) {
5670                      $modstatus = $moddeleteuserdata($data);
5671                      if (is_array($modstatus)) {
5672                          $status = array_merge($status, $modstatus);
5673                      } else {
5674                          debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5675                      }
5676                  } else {
5677                      $unsupportedmods[] = $mod;
5678                  }
5679              } else {
5680                  debugging('Missing lib.php in '.$modname.' module!');
5681              }
5682              // Update calendar events for all modules.
5683              course_module_bulk_update_calendar_events($modname, $data->courseid);
5684          }
5685          // Purge the course cache after resetting course start date. MDL-76936
5686          if ($data->timeshift) {
5687              course_modinfo::purge_course_cache($data->courseid);
5688          }
5689      }
5690  
5691      // Mention unsupported mods.
5692      if (!empty($unsupportedmods)) {
5693          foreach ($unsupportedmods as $mod) {
5694              $status[] = array(
5695                  'component' => get_string('modulenameplural', $mod->name),
5696                  'item' => '',
5697                  'error' => get_string('resetnotimplemented')
5698              );
5699          }
5700      }
5701  
5702      $componentstr = get_string('gradebook', 'grades');
5703      // Reset gradebook,.
5704      if (!empty($data->reset_gradebook_items)) {
5705          remove_course_grades($data->courseid, false);
5706          grade_grab_course_grades($data->courseid);
5707          grade_regrade_final_grades($data->courseid);
5708          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5709  
5710      } else if (!empty($data->reset_gradebook_grades)) {
5711          grade_course_reset($data->courseid);
5712          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5713      }
5714      // Reset comments.
5715      if (!empty($data->reset_comments)) {
5716          require_once($CFG->dirroot.'/comment/lib.php');
5717          comment::reset_course_page_comments($context);
5718      }
5719  
5720      $event = \core\event\course_reset_ended::create($eventparams);
5721      $event->trigger();
5722  
5723      return $status;
5724  }
5725  
5726  /**
5727   * Generate an email processing address.
5728   *
5729   * @param int $modid
5730   * @param string $modargs
5731   * @return string Returns email processing address
5732   */
5733  function generate_email_processing_address($modid, $modargs) {
5734      global $CFG;
5735  
5736      $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5737      return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5738  }
5739  
5740  /**
5741   * ?
5742   *
5743   * @todo Finish documenting this function
5744   *
5745   * @param string $modargs
5746   * @param string $body Currently unused
5747   */
5748  function moodle_process_email($modargs, $body) {
5749      global $DB;
5750  
5751      // The first char should be an unencoded letter. We'll take this as an action.
5752      switch ($modargs[0]) {
5753          case 'B': { // Bounce.
5754              list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5755              if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5756                  // Check the half md5 of their email.
5757                  $md5check = substr(md5($user->email), 0, 16);
5758                  if ($md5check == substr($modargs, -16)) {
5759                      set_bounce_count($user);
5760                  }
5761                  // Else maybe they've already changed it?
5762              }
5763          }
5764          break;
5765          // Maybe more later?
5766      }
5767  }
5768  
5769  // CORRESPONDENCE.
5770  
5771  /**
5772   * Get mailer instance, enable buffering, flush buffer or disable buffering.
5773   *
5774   * @param string $action 'get', 'buffer', 'close' or 'flush'
5775   * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5776   */
5777  function get_mailer($action='get') {
5778      global $CFG;
5779  
5780      /** @var moodle_phpmailer $mailer */
5781      static $mailer  = null;
5782      static $counter = 0;
5783  
5784      if (!isset($CFG->smtpmaxbulk)) {
5785          $CFG->smtpmaxbulk = 1;
5786      }
5787  
5788      if ($action == 'get') {
5789          $prevkeepalive = false;
5790  
5791          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5792              if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5793                  $counter++;
5794                  // Reset the mailer.
5795                  $mailer->Priority         = 3;
5796                  $mailer->CharSet          = 'UTF-8'; // Our default.
5797                  $mailer->ContentType      = "text/plain";
5798                  $mailer->Encoding         = "8bit";
5799                  $mailer->From             = "root@localhost";
5800                  $mailer->FromName         = "Root User";
5801                  $mailer->Sender           = "";
5802                  $mailer->Subject          = "";
5803                  $mailer->Body             = "";
5804                  $mailer->AltBody          = "";
5805                  $mailer->ConfirmReadingTo = "";
5806  
5807                  $mailer->clearAllRecipients();
5808                  $mailer->clearReplyTos();
5809                  $mailer->clearAttachments();
5810                  $mailer->clearCustomHeaders();
5811                  return $mailer;
5812              }
5813  
5814              $prevkeepalive = $mailer->SMTPKeepAlive;
5815              get_mailer('flush');
5816          }
5817  
5818          require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5819          $mailer = new moodle_phpmailer();
5820  
5821          $counter = 1;
5822  
5823          if ($CFG->smtphosts == 'qmail') {
5824              // Use Qmail system.
5825              $mailer->isQmail();
5826  
5827          } else if (empty($CFG->smtphosts)) {
5828              // Use PHP mail() = sendmail.
5829              $mailer->isMail();
5830  
5831          } else {
5832              // Use SMTP directly.
5833              $mailer->isSMTP();
5834              if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5835                  $mailer->SMTPDebug = 3;
5836              }
5837              // Specify main and backup servers.
5838              $mailer->Host          = $CFG->smtphosts;
5839              // Specify secure connection protocol.
5840              $mailer->SMTPSecure    = $CFG->smtpsecure;
5841              // Use previous keepalive.
5842              $mailer->SMTPKeepAlive = $prevkeepalive;
5843  
5844              if ($CFG->smtpuser) {
5845                  // Use SMTP authentication.
5846                  $mailer->SMTPAuth = true;
5847                  $mailer->Username = $CFG->smtpuser;
5848                  $mailer->Password = $CFG->smtppass;
5849              }
5850          }
5851  
5852          return $mailer;
5853      }
5854  
5855      $nothing = null;
5856  
5857      // Keep smtp session open after sending.
5858      if ($action == 'buffer') {
5859          if (!empty($CFG->smtpmaxbulk)) {
5860              get_mailer('flush');
5861              $m = get_mailer();
5862              if ($m->Mailer == 'smtp') {
5863                  $m->SMTPKeepAlive = true;
5864              }
5865          }
5866          return $nothing;
5867      }
5868  
5869      // Close smtp session, but continue buffering.
5870      if ($action == 'flush') {
5871          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5872              if (!empty($mailer->SMTPDebug)) {
5873                  echo '<pre>'."\n";
5874              }
5875              $mailer->SmtpClose();
5876              if (!empty($mailer->SMTPDebug)) {
5877                  echo '</pre>';
5878              }
5879          }
5880          return $nothing;
5881      }
5882  
5883      // Close smtp session, do not buffer anymore.
5884      if ($action == 'close') {
5885          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5886              get_mailer('flush');
5887              $mailer->SMTPKeepAlive = false;
5888          }
5889          $mailer = null; // Better force new instance.
5890          return $nothing;
5891      }
5892  }
5893  
5894  /**
5895   * A helper function to test for email diversion
5896   *
5897   * @param string $email
5898   * @return bool Returns true if the email should be diverted
5899   */
5900  function email_should_be_diverted($email) {
5901      global $CFG;
5902  
5903      if (empty($CFG->divertallemailsto)) {
5904          return false;
5905      }
5906  
5907      if (empty($CFG->divertallemailsexcept)) {
5908          return true;
5909      }
5910  
5911      $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5912      foreach ($patterns as $pattern) {
5913          if (preg_match("/{$pattern}/i", $email)) {
5914              return false;
5915          }
5916      }
5917  
5918      return true;
5919  }
5920  
5921  /**
5922   * Generate a unique email Message-ID using the moodle domain and install path
5923   *
5924   * @param string $localpart An optional unique message id prefix.
5925   * @return string The formatted ID ready for appending to the email headers.
5926   */
5927  function generate_email_messageid($localpart = null) {
5928      global $CFG;
5929  
5930      $urlinfo = parse_url($CFG->wwwroot);
5931      $base = '@' . $urlinfo['host'];
5932  
5933      // If multiple moodles are on the same domain we want to tell them
5934      // apart so we add the install path to the local part. This means
5935      // that the id local part should never contain a / character so
5936      // we can correctly parse the id to reassemble the wwwroot.
5937      if (isset($urlinfo['path'])) {
5938          $base = $urlinfo['path'] . $base;
5939      }
5940  
5941      if (empty($localpart)) {
5942          $localpart = uniqid('', true);
5943      }
5944  
5945      // Because we may have an option /installpath suffix to the local part
5946      // of the id we need to escape any / chars which are in the $localpart.
5947      $localpart = str_replace('/', '%2F', $localpart);
5948  
5949      return '<' . $localpart . $base . '>';
5950  }
5951  
5952  /**
5953   * Send an email to a specified user
5954   *
5955   * @param stdClass $user  A {@link $USER} object
5956   * @param stdClass $from A {@link $USER} object
5957   * @param string $subject plain text subject line of the email
5958   * @param string $messagetext plain text version of the message
5959   * @param string $messagehtml complete html version of the message (optional)
5960   * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5961   *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5962   * @param string $attachname the name of the file (extension indicates MIME)
5963   * @param bool $usetrueaddress determines whether $from email address should
5964   *          be sent out. Will be overruled by user profile setting for maildisplay
5965   * @param string $replyto Email address to reply to
5966   * @param string $replytoname Name of reply to recipient
5967   * @param int $wordwrapwidth custom word wrap width, default 79
5968   * @return bool Returns true if mail was sent OK and false if there was an error.
5969   */
5970  function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5971                         $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5972  
5973      global $CFG, $PAGE, $SITE;
5974  
5975      if (empty($user) or empty($user->id)) {
5976          debugging('Can not send email to null user', DEBUG_DEVELOPER);
5977          return false;
5978      }
5979  
5980      if (empty($user->email)) {
5981          debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5982          return false;
5983      }
5984  
5985      if (!empty($user->deleted)) {
5986          debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5987          return false;
5988      }
5989  
5990      if (defined('BEHAT_SITE_RUNNING')) {
5991          // Fake email sending in behat.
5992          return true;
5993      }
5994  
5995      if (!empty($CFG->noemailever)) {
5996          // Hidden setting for development sites, set in config.php if needed.
5997          debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5998          return true;
5999      }
6000  
6001      if (email_should_be_diverted($user->email)) {
6002          $subject = "[DIVERTED {$user->email}] $subject";
6003          $user = clone($user);
6004          $user->email = $CFG->divertallemailsto;
6005      }
6006  
6007      // Skip mail to suspended users.
6008      if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
6009          return true;
6010      }
6011  
6012      if (!validate_email($user->email)) {
6013          // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6014          debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6015          return false;
6016      }
6017  
6018      if (over_bounce_threshold($user)) {
6019          debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6020          return false;
6021      }
6022  
6023      // TLD .invalid  is specifically reserved for invalid domain names.
6024      // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6025      if (substr($user->email, -8) == '.invalid') {
6026          debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6027          return true; // This is not an error.
6028      }
6029  
6030      // If the user is a remote mnet user, parse the email text for URL to the
6031      // wwwroot and modify the url to direct the user's browser to login at their
6032      // home site (identity provider - idp) before hitting the link itself.
6033      if (is_mnet_remote_user($user)) {
6034          require_once($CFG->dirroot.'/mnet/lib.php');
6035  
6036          $jumpurl = mnet_get_idp_jump_url($user);
6037          $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6038  
6039          $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6040                  $callback,
6041                  $messagetext);
6042          $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6043                  $callback,
6044                  $messagehtml);
6045      }
6046      $mail = get_mailer();
6047  
6048      if (!empty($mail->SMTPDebug)) {
6049          echo '<pre>' . "\n";
6050      }
6051  
6052      $temprecipients = array();
6053      $tempreplyto = array();
6054  
6055      // Make sure that we fall back onto some reasonable no-reply address.
6056      $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6057      $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6058  
6059      if (!validate_email($noreplyaddress)) {
6060          debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6061          $noreplyaddress = $noreplyaddressdefault;
6062      }
6063  
6064      // Make up an email address for handling bounces.
6065      if (!empty($CFG->handlebounces)) {
6066          $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6067          $mail->Sender = generate_email_processing_address(0, $modargs);
6068      } else {
6069          $mail->Sender = $noreplyaddress;
6070      }
6071  
6072      // Make sure that the explicit replyto is valid, fall back to the implicit one.
6073      if (!empty($replyto) && !validate_email($replyto)) {
6074          debugging('email_to_user: Invalid replyto-email '.s($replyto));
6075          $replyto = $noreplyaddress;
6076      }
6077  
6078      if (is_string($from)) { // So we can pass whatever we want if there is need.
6079          $mail->From     = $noreplyaddress;
6080          $mail->FromName = $from;
6081      // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6082      // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6083      // in a course with the sender.
6084      } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6085          if (!validate_email($from->email)) {
6086              debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6087              // Better not to use $noreplyaddress in this case.
6088              return false;
6089          }
6090          $mail->From = $from->email;
6091          $fromdetails = new stdClass();
6092          $fromdetails->name = fullname($from);
6093          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6094          $fromdetails->siteshortname = format_string($SITE->shortname);
6095          $fromstring = $fromdetails->name;
6096          if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6097              $fromstring = get_string('emailvia', 'core', $fromdetails);
6098          }
6099          $mail->FromName = $fromstring;
6100          if (empty($replyto)) {
6101              $tempreplyto[] = array($from->email, fullname($from));
6102          }
6103      } else {
6104          $mail->From = $noreplyaddress;
6105          $fromdetails = new stdClass();
6106          $fromdetails->name = fullname($from);
6107          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6108          $fromdetails->siteshortname = format_string($SITE->shortname);
6109          $fromstring = $fromdetails->name;
6110          if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6111              $fromstring = get_string('emailvia', 'core', $fromdetails);
6112          }
6113          $mail->FromName = $fromstring;
6114          if (empty($replyto)) {
6115              $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6116          }
6117      }
6118  
6119      if (!empty($replyto)) {
6120          $tempreplyto[] = array($replyto, $replytoname);
6121      }
6122  
6123      $temprecipients[] = array($user->email, fullname($user));
6124  
6125      // Set word wrap.
6126      $mail->WordWrap = $wordwrapwidth;
6127  
6128      if (!empty($from->customheaders)) {
6129          // Add custom headers.
6130          if (is_array($from->customheaders)) {
6131              foreach ($from->customheaders as $customheader) {
6132                  $mail->addCustomHeader($customheader);
6133              }
6134          } else {
6135              $mail->addCustomHeader($from->customheaders);
6136          }
6137      }
6138  
6139      // If the X-PHP-Originating-Script email header is on then also add an additional
6140      // header with details of where exactly in moodle the email was triggered from,
6141      // either a call to message_send() or to email_to_user().
6142      if (ini_get('mail.add_x_header')) {
6143  
6144          $stack = debug_backtrace(false);
6145          $origin = $stack[0];
6146  
6147          foreach ($stack as $depth => $call) {
6148              if ($call['function'] == 'message_send') {
6149                  $origin = $call;
6150              }
6151          }
6152  
6153          $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6154               . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6155          $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6156      }
6157  
6158      if (!empty($CFG->emailheaders)) {
6159          $headers = array_map('trim', explode("\n", $CFG->emailheaders));
6160          foreach ($headers as $header) {
6161              if (!empty($header)) {
6162                  $mail->addCustomHeader($header);
6163              }
6164          }
6165      }
6166  
6167      if (!empty($from->priority)) {
6168          $mail->Priority = $from->priority;
6169      }
6170  
6171      $renderer = $PAGE->get_renderer('core');
6172      $context = array(
6173          'sitefullname' => $SITE->fullname,
6174          'siteshortname' => $SITE->shortname,
6175          'sitewwwroot' => $CFG->wwwroot,
6176          'subject' => $subject,
6177          'prefix' => $CFG->emailsubjectprefix,
6178          'to' => $user->email,
6179          'toname' => fullname($user),
6180          'from' => $mail->From,
6181          'fromname' => $mail->FromName,
6182      );
6183      if (!empty($tempreplyto[0])) {
6184          $context['replyto'] = $tempreplyto[0][0];
6185          $context['replytoname'] = $tempreplyto[0][1];
6186      }
6187      if ($user->id > 0) {
6188          $context['touserid'] = $user->id;
6189          $context['tousername'] = $user->username;
6190      }
6191  
6192      if (!empty($user->mailformat) && $user->mailformat == 1) {
6193          // Only process html templates if the user preferences allow html email.
6194  
6195          if (!$messagehtml) {
6196              // If no html has been given, BUT there is an html wrapping template then
6197              // auto convert the text to html and then wrap it.
6198              $messagehtml = trim(text_to_html($messagetext));
6199          }
6200          $context['body'] = $messagehtml;
6201          $messagehtml = $renderer->render_from_template('core/email_html', $context);
6202      }
6203  
6204      $context['body'] = html_to_text(nl2br($messagetext));
6205      $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6206      $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6207      $messagetext = $renderer->render_from_template('core/email_text', $context);
6208  
6209      // Autogenerate a MessageID if it's missing.
6210      if (empty($mail->MessageID)) {
6211          $mail->MessageID = generate_email_messageid();
6212      }
6213  
6214      if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6215          // Don't ever send HTML to users who don't want it.
6216          $mail->isHTML(true);
6217          $mail->Encoding = 'quoted-printable';
6218          $mail->Body    =  $messagehtml;
6219          $mail->AltBody =  "\n$messagetext\n";
6220      } else {
6221          $mail->IsHTML(false);
6222          $mail->Body =  "\n$messagetext\n";
6223      }
6224  
6225      if ($attachment && $attachname) {
6226          if (preg_match( "~\\.\\.~" , $attachment )) {
6227              // Security check for ".." in dir path.
6228              $supportuser = core_user::get_support_user();
6229              $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6230              $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6231          } else {
6232              require_once($CFG->libdir.'/filelib.php');
6233              $mimetype = mimeinfo('type', $attachname);
6234  
6235              // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6236              // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6237              $attachpath = str_replace('\\', '/', realpath($attachment));
6238  
6239              // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6240              $allowedpaths = array_map(function(string $path): string {
6241                  return str_replace('\\', '/', realpath($path));
6242              }, [
6243                  $CFG->cachedir,
6244                  $CFG->dataroot,
6245                  $CFG->dirroot,
6246                  $CFG->localcachedir,
6247                  $CFG->tempdir,
6248                  $CFG->localrequestdir,
6249              ]);
6250  
6251              // Set addpath to true.
6252              $addpath = true;
6253  
6254              // Check if attachment includes one of the allowed paths.
6255              foreach (array_filter($allowedpaths) as $allowedpath) {
6256                  // Set addpath to false if the attachment includes one of the allowed paths.
6257                  if (strpos($attachpath, $allowedpath) === 0) {
6258                      $addpath = false;
6259                      break;
6260                  }
6261              }
6262  
6263              // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6264              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6265              if ($addpath == true) {
6266                  $attachment = $CFG->dataroot . '/' . $attachment;
6267              }
6268  
6269              $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6270          }
6271      }
6272  
6273      // Check if the email should be sent in an other charset then the default UTF-8.
6274      if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6275  
6276          // Use the defined site mail charset or eventually the one preferred by the recipient.
6277          $charset = $CFG->sitemailcharset;
6278          if (!empty($CFG->allowusermailcharset)) {
6279              if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6280                  $charset = $useremailcharset;
6281              }
6282          }
6283  
6284          // Convert all the necessary strings if the charset is supported.
6285          $charsets = get_list_of_charsets();
6286          unset($charsets['UTF-8']);
6287          if (in_array($charset, $charsets)) {
6288              $mail->CharSet  = $charset;
6289              $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6290              $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6291              $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6292              $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6293  
6294              foreach ($temprecipients as $key => $values) {
6295                  $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6296              }
6297              foreach ($tempreplyto as $key => $values) {
6298                  $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6299              }
6300          }
6301      }
6302  
6303      foreach ($temprecipients as $values) {
6304          $mail->addAddress($values[0], $values[1]);
6305      }
6306      foreach ($tempreplyto as $values) {
6307          $mail->addReplyTo($values[0], $values[1]);
6308      }
6309  
6310      if (!empty($CFG->emaildkimselector)) {
6311          $domain = substr(strrchr($mail->From, "@"), 1);
6312          $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6313          if (file_exists($pempath)) {
6314              $mail->DKIM_domain      = $domain;
6315              $mail->DKIM_private     = $pempath;
6316              $mail->DKIM_selector    = $CFG->emaildkimselector;
6317              $mail->DKIM_identity    = $mail->From;
6318          } else {
6319              debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6320          }
6321      }
6322  
6323      if ($mail->send()) {
6324          set_send_count($user);
6325          if (!empty($mail->SMTPDebug)) {
6326              echo '</pre>';
6327          }
6328          return true;
6329      } else {
6330          // Trigger event for failing to send email.
6331          $event = \core\event\email_failed::create(array(
6332              'context' => context_system::instance(),
6333              'userid' => $from->id,
6334              'relateduserid' => $user->id,
6335              'other' => array(
6336                  'subject' => $subject,
6337                  'message' => $messagetext,
6338                  'errorinfo' => $mail->ErrorInfo
6339              )
6340          ));
6341          $event->trigger();
6342          if (CLI_SCRIPT) {
6343              mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6344          }
6345          if (!empty($mail->SMTPDebug)) {
6346              echo '</pre>';
6347          }
6348          return false;
6349      }
6350  }
6351  
6352  /**
6353   * Check to see if a user's real email address should be used for the "From" field.
6354   *
6355   * @param  object $from The user object for the user we are sending the email from.
6356   * @param  object $user The user object that we are sending the email to.
6357   * @param  array $unused No longer used.
6358   * @return bool Returns true if we can use the from user's email adress in the "From" field.
6359   */
6360  function can_send_from_real_email_address($from, $user, $unused = null) {
6361      global $CFG;
6362      if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6363          return false;
6364      }
6365      $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6366      // Email is in the list of allowed domains for sending email,
6367      // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6368      // in a course with the sender.
6369      if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6370                  && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6371                  || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6372                  && enrol_get_shared_courses($user, $from, false, true)))) {
6373          return true;
6374      }
6375      return false;
6376  }
6377  
6378  /**
6379   * Generate a signoff for emails based on support settings
6380   *
6381   * @return string
6382   */
6383  function generate_email_signoff() {
6384      global $CFG, $OUTPUT;
6385  
6386      $signoff = "\n";
6387      if (!empty($CFG->supportname)) {
6388          $signoff .= $CFG->supportname."\n";
6389      }
6390  
6391      $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6392  
6393      if ($supportemail) {
6394          $signoff .= "\n" . $supportemail . "\n";
6395      }
6396  
6397      return $signoff;
6398  }
6399  
6400  /**
6401   * Sets specified user's password and send the new password to the user via email.
6402   *
6403   * @param stdClass $user A {@link $USER} object
6404   * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6405   * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6406   */
6407  function setnew_password_and_mail($user, $fasthash = false) {
6408      global $CFG, $DB;
6409  
6410      // We try to send the mail in language the user understands,
6411      // unfortunately the filter_string() does not support alternative langs yet
6412      // so multilang will not work properly for site->fullname.
6413      $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6414  
6415      $site  = get_site();
6416  
6417      $supportuser = core_user::get_support_user();
6418  
6419      $newpassword = generate_password();
6420  
6421      update_internal_user_password($user, $newpassword, $fasthash);
6422  
6423      $a = new stdClass();
6424      $a->firstname   = fullname($user, true);
6425      $a->sitename    = format_string($site->fullname);
6426      $a->username    = $user->username;
6427      $a->newpassword = $newpassword;
6428      $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6429      $a->signoff     = generate_email_signoff();
6430  
6431      $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6432  
6433      $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6434  
6435      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6436      return email_to_user($user, $supportuser, $subject, $message);
6437  
6438  }
6439  
6440  /**
6441   * Resets specified user's password and send the new password to the user via email.
6442   *
6443   * @param stdClass $user A {@link $USER} object
6444   * @return bool Returns true if mail was sent OK and false if there was an error.
6445   */
6446  function reset_password_and_mail($user) {
6447      global $CFG;
6448  
6449      $site  = get_site();
6450      $supportuser = core_user::get_support_user();
6451  
6452      $userauth = get_auth_plugin($user->auth);
6453      if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6454          trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6455          return false;
6456      }
6457  
6458      $newpassword = generate_password();
6459  
6460      if (!$userauth->user_update_password($user, $newpassword)) {
6461          throw new \moodle_exception("cannotsetpassword");
6462      }
6463  
6464      $a = new stdClass();
6465      $a->firstname   = $user->firstname;
6466      $a->lastname    = $user->lastname;
6467      $a->sitename    = format_string($site->fullname);
6468      $a->username    = $user->username;
6469      $a->newpassword = $newpassword;
6470      $a->link        = $CFG->wwwroot .'/login/change_password.php';
6471      $a->signoff     = generate_email_signoff();
6472  
6473      $message = get_string('newpasswordtext', '', $a);
6474  
6475      $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6476  
6477      unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6478  
6479      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6480      return email_to_user($user, $supportuser, $subject, $message);
6481  }
6482  
6483  /**
6484   * Send email to specified user with confirmation text and activation link.
6485   *
6486   * @param stdClass $user A {@link $USER} object
6487   * @param string $confirmationurl user confirmation URL
6488   * @return bool Returns true if mail was sent OK and false if there was an error.
6489   */
6490  function send_confirmation_email($user, $confirmationurl = null) {
6491      global $CFG;
6492  
6493      $site = get_site();
6494      $supportuser = core_user::get_support_user();
6495  
6496      $data = new stdClass();
6497      $data->sitename  = format_string($site->fullname);
6498      $data->admin     = generate_email_signoff();
6499  
6500      $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6501  
6502      if (empty($confirmationurl)) {
6503          $confirmationurl = '/login/confirm.php';
6504      }
6505  
6506      $confirmationurl = new moodle_url($confirmationurl);
6507      // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6508      $confirmationurl->remove_params('data');
6509      $confirmationpath = $confirmationurl->out(false);
6510  
6511      // We need to custom encode the username to include trailing dots in the link.
6512      // Because of this custom encoding we can't use moodle_url directly.
6513      // Determine if a query string is present in the confirmation url.
6514      $hasquerystring = strpos($confirmationpath, '?') !== false;
6515      // Perform normal url encoding of the username first.
6516      $username = urlencode($user->username);
6517      // Prevent problems with trailing dots not being included as part of link in some mail clients.
6518      $username = str_replace('.', '%2E', $username);
6519  
6520      $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6521  
6522      $message     = get_string('emailconfirmation', '', $data);
6523      $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6524  
6525      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6526      return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6527  }
6528  
6529  /**
6530   * Sends a password change confirmation email.
6531   *
6532   * @param stdClass $user A {@link $USER} object
6533   * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6534   * @return bool Returns true if mail was sent OK and false if there was an error.
6535   */
6536  function send_password_change_confirmation_email($user, $resetrecord) {
6537      global $CFG;
6538  
6539      $site = get_site();
6540      $supportuser = core_user::get_support_user();
6541      $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6542  
6543      $data = new stdClass();
6544      $data->firstname = $user->firstname;
6545      $data->lastname  = $user->lastname;
6546      $data->username  = $user->username;
6547      $data->sitename  = format_string($site->fullname);
6548      $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6549      $data->admin     = generate_email_signoff();
6550      $data->resetminutes = $pwresetmins;
6551  
6552      $message = get_string('emailresetconfirmation', '', $data);
6553      $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6554  
6555      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6556      return email_to_user($user, $supportuser, $subject, $message);
6557  
6558  }
6559  
6560  /**
6561   * Sends an email containing information on how to change your password.
6562   *
6563   * @param stdClass $user A {@link $USER} object
6564   * @return bool Returns true if mail was sent OK and false if there was an error.
6565   */
6566  function send_password_change_info($user) {
6567      $site = get_site();
6568      $supportuser = core_user::get_support_user();
6569  
6570      $data = new stdClass();
6571      $data->firstname = $user->firstname;
6572      $data->lastname  = $user->lastname;
6573      $data->username  = $user->username;
6574      $data->sitename  = format_string($site->fullname);
6575      $data->admin     = generate_email_signoff();
6576  
6577      if (!is_enabled_auth($user->auth)) {
6578          $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6579          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
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      $userauth = get_auth_plugin($user->auth);
6585      ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6586  
6587      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6588      return email_to_user($user, $supportuser, $subject, $message);
6589  }
6590  
6591  /**
6592   * Check that an email is allowed.  It returns an error message if there was a problem.
6593   *
6594   * @param string $email Content of email
6595   * @return string|false
6596   */
6597  function email_is_not_allowed($email) {
6598      global $CFG;
6599  
6600      // Comparing lowercase domains.
6601      $email = strtolower($email);
6602      if (!empty($CFG->allowemailaddresses)) {
6603          $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6604          foreach ($allowed as $allowedpattern) {
6605              $allowedpattern = trim($allowedpattern);
6606              if (!$allowedpattern) {
6607                  continue;
6608              }
6609              if (strpos($allowedpattern, '.') === 0) {
6610                  if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6611                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6612                      return false;
6613                  }
6614  
6615              } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6616                  return false;
6617              }
6618          }
6619          return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6620  
6621      } else if (!empty($CFG->denyemailaddresses)) {
6622          $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6623          foreach ($denied as $deniedpattern) {
6624              $deniedpattern = trim($deniedpattern);
6625              if (!$deniedpattern) {
6626                  continue;
6627              }
6628              if (strpos($deniedpattern, '.') === 0) {
6629                  if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6630                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6631                      return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6632                  }
6633  
6634              } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6635                  return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6636              }
6637          }
6638      }
6639  
6640      return false;
6641  }
6642  
6643  // FILE HANDLING.
6644  
6645  /**
6646   * Returns local file storage instance
6647   *
6648   * @return file_storage
6649   */
6650  function get_file_storage($reset = false) {
6651      global $CFG;
6652  
6653      static $fs = null;
6654  
6655      if ($reset) {
6656          $fs = null;
6657          return;
6658      }
6659  
6660      if ($fs) {
6661          return $fs;
6662      }
6663  
6664      require_once("$CFG->libdir/filelib.php");
6665  
6666      $fs = new file_storage();
6667  
6668      return $fs;
6669  }
6670  
6671  /**
6672   * Returns local file storage instance
6673   *
6674   * @return file_browser
6675   */
6676  function get_file_browser() {
6677      global $CFG;
6678  
6679      static $fb = null;
6680  
6681      if ($fb) {
6682          return $fb;
6683      }
6684  
6685      require_once("$CFG->libdir/filelib.php");
6686  
6687      $fb = new file_browser();
6688  
6689      return $fb;
6690  }
6691  
6692  /**
6693   * Returns file packer
6694   *
6695   * @param string $mimetype default application/zip
6696   * @return file_packer
6697   */
6698  function get_file_packer($mimetype='application/zip') {
6699      global $CFG;
6700  
6701      static $fp = array();
6702  
6703      if (isset($fp[$mimetype])) {
6704          return $fp[$mimetype];
6705      }
6706  
6707      switch ($mimetype) {
6708          case 'application/zip':
6709          case 'application/vnd.moodle.profiling':
6710              $classname = 'zip_packer';
6711              break;
6712  
6713          case 'application/x-gzip' :
6714              $classname = 'tgz_packer';
6715              break;
6716  
6717          case 'application/vnd.moodle.backup':
6718              $classname = 'mbz_packer';
6719              break;
6720  
6721          default:
6722              return false;
6723      }
6724  
6725      require_once("$CFG->libdir/filestorage/$classname.php");
6726      $fp[$mimetype] = new $classname();
6727  
6728      return $fp[$mimetype];
6729  }
6730  
6731  /**
6732   * Returns current name of file on disk if it exists.
6733   *
6734   * @param string $newfile File to be verified
6735   * @return string Current name of file on disk if true
6736   */
6737  function valid_uploaded_file($newfile) {
6738      if (empty($newfile)) {
6739          return '';
6740      }
6741      if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6742          return $newfile['tmp_name'];
6743      } else {
6744          return '';
6745      }
6746  }
6747  
6748  /**
6749   * Returns the maximum size for uploading files.
6750   *
6751   * There are seven possible upload limits:
6752   * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6753   * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6754   * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6755   * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6756   * 5. by the Moodle admin in $CFG->maxbytes
6757   * 6. by the teacher in the current course $course->maxbytes
6758   * 7. by the teacher for the current module, eg $assignment->maxbytes
6759   *
6760   * These last two are passed to this function as arguments (in bytes).
6761   * Anything defined as 0 is ignored.
6762   * The smallest of all the non-zero numbers is returned.
6763   *
6764   * @todo Finish documenting this function
6765   *
6766   * @param int $sitebytes Set maximum size
6767   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6768   * @param int $modulebytes Current module ->maxbytes (in bytes)
6769   * @param bool $unused This parameter has been deprecated and is not used any more.
6770   * @return int The maximum size for uploading files.
6771   */
6772  function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6773  
6774      if (! $filesize = ini_get('upload_max_filesize')) {
6775          $filesize = '5M';
6776      }
6777      $minimumsize = get_real_size($filesize);
6778  
6779      if ($postsize = ini_get('post_max_size')) {
6780          $postsize = get_real_size($postsize);
6781          if ($postsize < $minimumsize) {
6782              $minimumsize = $postsize;
6783          }
6784      }
6785  
6786      if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6787          $minimumsize = $sitebytes;
6788      }
6789  
6790      if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6791          $minimumsize = $coursebytes;
6792      }
6793  
6794      if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6795          $minimumsize = $modulebytes;
6796      }
6797  
6798      return $minimumsize;
6799  }
6800  
6801  /**
6802   * Returns the maximum size for uploading files for the current user
6803   *
6804   * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6805   *
6806   * @param context $context The context in which to check user capabilities
6807   * @param int $sitebytes Set maximum size
6808   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6809   * @param int $modulebytes Current module ->maxbytes (in bytes)
6810   * @param stdClass $user The user
6811   * @param bool $unused This parameter has been deprecated and is not used any more.
6812   * @return int The maximum size for uploading files.
6813   */
6814  function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6815          $unused = false) {
6816      global $USER;
6817  
6818      if (empty($user)) {
6819          $user = $USER;
6820      }
6821  
6822      if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6823          return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6824      }
6825  
6826      return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6827  }
6828  
6829  /**
6830   * Returns an array of possible sizes in local language
6831   *
6832   * Related to {@link get_max_upload_file_size()} - this function returns an
6833   * array of possible sizes in an array, translated to the
6834   * local language.
6835   *
6836   * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6837   *
6838   * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6839   * with the value set to 0. This option will be the first in the list.
6840   *
6841   * @uses SORT_NUMERIC
6842   * @param int $sitebytes Set maximum size
6843   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6844   * @param int $modulebytes Current module ->maxbytes (in bytes)
6845   * @param int|array $custombytes custom upload size/s which will be added to list,
6846   *        Only value/s smaller then maxsize will be added to list.
6847   * @return array
6848   */
6849  function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6850      global $CFG;
6851  
6852      if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6853          return array();
6854      }
6855  
6856      if ($sitebytes == 0) {
6857          // Will get the minimum of upload_max_filesize or post_max_size.
6858          $sitebytes = get_max_upload_file_size();
6859      }
6860  
6861      $filesize = array();
6862      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6863                        5242880, 10485760, 20971520, 52428800, 104857600,
6864                        262144000, 524288000, 786432000, 1073741824,
6865                        2147483648, 4294967296, 8589934592);
6866  
6867      // If custombytes is given and is valid then add it to the list.
6868      if (is_number($custombytes) and $custombytes > 0) {
6869          $custombytes = (int)$custombytes;
6870          if (!in_array($custombytes, $sizelist)) {
6871              $sizelist[] = $custombytes;
6872          }
6873      } else if (is_array($custombytes)) {
6874          $sizelist = array_unique(array_merge($sizelist, $custombytes));
6875      }
6876  
6877      // Allow maxbytes to be selected if it falls outside the above boundaries.
6878      if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6879          // Note: get_real_size() is used in order to prevent problems with invalid values.
6880          $sizelist[] = get_real_size($CFG->maxbytes);
6881      }
6882  
6883      foreach ($sizelist as $sizebytes) {
6884          if ($sizebytes < $maxsize && $sizebytes > 0) {
6885              $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6886          }
6887      }
6888  
6889      $limitlevel = '';
6890      $displaysize = '';
6891      if ($modulebytes &&
6892          (($modulebytes < $coursebytes || $coursebytes == 0) &&
6893           ($modulebytes < $sitebytes || $sitebytes == 0))) {
6894          $limitlevel = get_string('activity', 'core');
6895          $displaysize = display_size($modulebytes, 0);
6896          $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6897  
6898      } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6899          $limitlevel = get_string('course', 'core');
6900          $displaysize = display_size($coursebytes, 0);
6901          $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6902  
6903      } else if ($sitebytes) {
6904          $limitlevel = get_string('site', 'core');
6905          $displaysize = display_size($sitebytes, 0);
6906          $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6907      }
6908  
6909      krsort($filesize, SORT_NUMERIC);
6910      if ($limitlevel) {
6911          $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6912          $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6913      }
6914  
6915      return $filesize;
6916  }
6917  
6918  /**
6919   * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6920   *
6921   * If excludefiles is defined, then that file/directory is ignored
6922   * If getdirs is true, then (sub)directories are included in the output
6923   * If getfiles is true, then files are included in the output
6924   * (at least one of these must be true!)
6925   *
6926   * @todo Finish documenting this function. Add examples of $excludefile usage.
6927   *
6928   * @param string $rootdir A given root directory to start from
6929   * @param string|array $excludefiles If defined then the specified file/directory is ignored
6930   * @param bool $descend If true then subdirectories are recursed as well
6931   * @param bool $getdirs If true then (sub)directories are included in the output
6932   * @param bool $getfiles  If true then files are included in the output
6933   * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6934   */
6935  function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6936  
6937      $dirs = array();
6938  
6939      if (!$getdirs and !$getfiles) {   // Nothing to show.
6940          return $dirs;
6941      }
6942  
6943      if (!is_dir($rootdir)) {          // Must be a directory.
6944          return $dirs;
6945      }
6946  
6947      if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6948          return $dirs;
6949      }
6950  
6951      if (!is_array($excludefiles)) {
6952          $excludefiles = array($excludefiles);
6953      }
6954  
6955      while (false !== ($file = readdir($dir))) {
6956          $firstchar = substr($file, 0, 1);
6957          if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6958              continue;
6959          }
6960          $fullfile = $rootdir .'/'. $file;
6961          if (filetype($fullfile) == 'dir') {
6962              if ($getdirs) {
6963                  $dirs[] = $file;
6964              }
6965              if ($descend) {
6966                  $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6967                  foreach ($subdirs as $subdir) {
6968                      $dirs[] = $file .'/'. $subdir;
6969                  }
6970              }
6971          } else if ($getfiles) {
6972              $dirs[] = $file;
6973          }
6974      }
6975      closedir($dir);
6976  
6977      asort($dirs);
6978  
6979      return $dirs;
6980  }
6981  
6982  
6983  /**
6984   * Adds up all the files in a directory and works out the size.
6985   *
6986   * @param string $rootdir  The directory to start from
6987   * @param string $excludefile A file to exclude when summing directory size
6988   * @return int The summed size of all files and subfiles within the root directory
6989   */
6990  function get_directory_size($rootdir, $excludefile='') {
6991      global $CFG;
6992  
6993      // Do it this way if we can, it's much faster.
6994      if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6995          $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6996          $output = null;
6997          $return = null;
6998          exec($command, $output, $return);
6999          if (is_array($output)) {
7000              // We told it to return k.
7001              return get_real_size(intval($output[0]).'k');
7002          }
7003      }
7004  
7005      if (!is_dir($rootdir)) {
7006          // Must be a directory.
7007          return 0;
7008      }
7009  
7010      if (!$dir = @opendir($rootdir)) {
7011          // Can't open it for some reason.
7012          return 0;
7013      }
7014  
7015      $size = 0;
7016  
7017      while (false !== ($file = readdir($dir))) {
7018          $firstchar = substr($file, 0, 1);
7019          if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7020              continue;
7021          }
7022          $fullfile = $rootdir .'/'. $file;
7023          if (filetype($fullfile) == 'dir') {
7024              $size += get_directory_size($fullfile, $excludefile);
7025          } else {
7026              $size += filesize($fullfile);
7027          }
7028      }
7029      closedir($dir);
7030  
7031      return $size;
7032  }
7033  
7034  /**
7035   * Converts bytes into display form
7036   *
7037   * @param int $size  The size to convert to human readable form
7038   * @param int $decimalplaces If specified, uses fixed number of decimal places
7039   * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
7040   * @return string Display version of size
7041   */
7042  function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
7043  
7044      static $units;
7045  
7046      if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
7047          return get_string('unlimited');
7048      }
7049  
7050      if (empty($units)) {
7051          $units[] = get_string('sizeb');
7052          $units[] = get_string('sizekb');
7053          $units[] = get_string('sizemb');
7054          $units[] = get_string('sizegb');
7055          $units[] = get_string('sizetb');
7056          $units[] = get_string('sizepb');
7057      }
7058  
7059      switch ($fixedunits) {
7060          case 'PB' :
7061              $magnitude = 5;
7062              break;
7063          case 'TB' :
7064              $magnitude = 4;
7065              break;
7066          case 'GB' :
7067              $magnitude = 3;
7068              break;
7069          case 'MB' :
7070              $magnitude = 2;
7071              break;
7072          case 'KB' :
7073              $magnitude = 1;
7074              break;
7075          case 'B' :
7076              $magnitude = 0;
7077              break;
7078          case '':
7079              $magnitude = floor(log($size, 1024));
7080              $magnitude = max(0, min(5, $magnitude));
7081              break;
7082          default:
7083              throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
7084      }
7085  
7086      // Special case for magnitude 0 (bytes) - never use decimal places.
7087      $nbsp = "\xc2\xa0";
7088      if ($magnitude === 0) {
7089          return round($size) . $nbsp . $units[$magnitude];
7090      }
7091  
7092      // Convert to specified units.
7093      $sizeinunit = $size / 1024 ** $magnitude;
7094  
7095      // Fixed decimal places.
7096      return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
7097  }
7098  
7099  /**
7100   * Cleans a given filename by removing suspicious or troublesome characters
7101   *
7102   * @see clean_param()
7103   * @param string $string file name
7104   * @return string cleaned file name
7105   */
7106  function clean_filename($string) {
7107      return clean_param($string, PARAM_FILE);
7108  }
7109  
7110  // STRING TRANSLATION.
7111  
7112  /**
7113   * Returns the code for the current language
7114   *
7115   * @category string
7116   * @return string
7117   */
7118  function current_language() {
7119      global $CFG, $PAGE, $SESSION, $USER;
7120  
7121      if (!empty($SESSION->forcelang)) {
7122          // Allows overriding course-forced language (useful for admins to check
7123          // issues in courses whose language they don't understand).
7124          // Also used by some code to temporarily get language-related information in a
7125          // specific language (see force_current_language()).
7126          $return = $SESSION->forcelang;
7127  
7128      } else if (!empty($PAGE->cm->lang)) {
7129          // Activity language, if set.
7130          $return = $PAGE->cm->lang;
7131  
7132      } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
7133          // Course language can override all other settings for this page.
7134          $return = $PAGE->course->lang;
7135  
7136      } else if (!empty($SESSION->lang)) {
7137          // Session language can override other settings.
7138          $return = $SESSION->lang;
7139  
7140      } else if (!empty($USER->lang)) {
7141          $return = $USER->lang;
7142  
7143      } else if (isset($CFG->lang)) {
7144          $return = $CFG->lang;
7145  
7146      } else {
7147          $return = 'en';
7148      }
7149  
7150      // Just in case this slipped in from somewhere by accident.
7151      $return = str_replace('_utf8', '', $return);
7152  
7153      return $return;
7154  }
7155  
7156  /**
7157   * Fix the current language to the given language code.
7158   *
7159   * @param string $lang The language code to use.
7160   * @return void
7161   */
7162  function fix_current_language(string $lang): void {
7163      global $CFG, $COURSE, $SESSION, $USER;
7164  
7165      if (!get_string_manager()->translation_exists($lang)) {
7166          throw new coding_exception("The language pack for $lang is not available");
7167      }
7168  
7169      $fixglobal = '';
7170      $fixlang = 'lang';
7171      if (!empty($SESSION->forcelang)) {
7172          $fixglobal = $SESSION;
7173          $fixlang = 'forcelang';
7174      } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
7175          $fixglobal = $COURSE;
7176      } else if (!empty($SESSION->lang)) {
7177          $fixglobal = $SESSION;
7178      } else if (!empty($USER->lang)) {
7179          $fixglobal = $USER;
7180      } else if (isset($CFG->lang)) {
7181          set_config('lang', $lang);
7182      }
7183  
7184      if ($fixglobal) {
7185          $fixglobal->$fixlang = $lang;
7186      }
7187  }
7188  
7189  /**
7190   * Returns parent language of current active language if defined
7191   *
7192   * @category string
7193   * @param string $lang null means current language
7194   * @return string
7195   */
7196  function get_parent_language($lang=null) {
7197  
7198      $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7199  
7200      if ($parentlang === 'en') {
7201          $parentlang = '';
7202      }
7203  
7204      return $parentlang;
7205  }
7206  
7207  /**
7208   * Force the current language to get strings and dates localised in the given language.
7209   *
7210   * After calling this function, all strings will be provided in the given language
7211   * until this function is called again, or equivalent code is run.
7212   *
7213   * @param string $language
7214   * @return string previous $SESSION->forcelang value
7215   */
7216  function force_current_language($language) {
7217      global $SESSION;
7218      $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7219      if ($language !== $sessionforcelang) {
7220          // Setting forcelang to null or an empty string disables its effect.
7221          if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7222              $SESSION->forcelang = $language;
7223              moodle_setlocale();
7224          }
7225      }
7226      return $sessionforcelang;
7227  }
7228  
7229  /**
7230   * Returns current string_manager instance.
7231   *
7232   * The param $forcereload is needed for CLI installer only where the string_manager instance
7233   * must be replaced during the install.php script life time.
7234   *
7235   * @category string
7236   * @param bool $forcereload shall the singleton be released and new instance created instead?
7237   * @return core_string_manager
7238   */
7239  function get_string_manager($forcereload=false) {
7240      global $CFG;
7241  
7242      static $singleton = null;
7243  
7244      if ($forcereload) {
7245          $singleton = null;
7246      }
7247      if ($singleton === null) {
7248          if (empty($CFG->early_install_lang)) {
7249  
7250              $transaliases = array();
7251              if (empty($CFG->langlist)) {
7252                   $translist = array();
7253              } else {
7254                  $translist = explode(',', $CFG->langlist);
7255                  $translist = array_map('trim', $translist);
7256                  // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7257                  foreach ($translist as $i => $value) {
7258                      $parts = preg_split('/\s*\|\s*/', $value, 2);
7259                      if (count($parts) == 2) {
7260                          $transaliases[$parts[0]] = $parts[1];
7261                          $translist[$i] = $parts[0];
7262                      }
7263                  }
7264              }
7265  
7266              if (!empty($CFG->config_php_settings['customstringmanager'])) {
7267                  $classname = $CFG->config_php_settings['customstringmanager'];
7268  
7269                  if (class_exists($classname)) {
7270                      $implements = class_implements($classname);
7271  
7272                      if (isset($implements['core_string_manager'])) {
7273                          $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7274                          return $singleton;
7275  
7276                      } else {
7277                          debugging('Unable to instantiate custom string manager: class '.$classname.
7278                              ' does not implement the core_string_manager interface.');
7279                      }
7280  
7281                  } else {
7282                      debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7283                  }
7284              }
7285  
7286              $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7287  
7288          } else {
7289              $singleton = new core_string_manager_install();
7290          }
7291      }
7292  
7293      return $singleton;
7294  }
7295  
7296  /**
7297   * Returns a localized string.
7298   *
7299   * Returns the translated string specified by $identifier as
7300   * for $module.  Uses the same format files as STphp.
7301   * $a is an object, string or number that can be used
7302   * within translation strings
7303   *
7304   * eg 'hello {$a->firstname} {$a->lastname}'
7305   * or 'hello {$a}'
7306   *
7307   * If you would like to directly echo the localized string use
7308   * the function {@link print_string()}
7309   *
7310   * Example usage of this function involves finding the string you would
7311   * like a local equivalent of and using its identifier and module information
7312   * to retrieve it.<br/>
7313   * If you open moodle/lang/en/moodle.php and look near line 278
7314   * you will find a string to prompt a user for their word for 'course'
7315   * <code>
7316   * $string['course'] = 'Course';
7317   * </code>
7318   * So if you want to display the string 'Course'
7319   * in any language that supports it on your site
7320   * you just need to use the identifier 'course'
7321   * <code>
7322   * $mystring = '<strong>'. get_string('course') .'</strong>';
7323   * or
7324   * </code>
7325   * If the string you want is in another file you'd take a slightly
7326   * different approach. Looking in moodle/lang/en/calendar.php you find
7327   * around line 75:
7328   * <code>
7329   * $string['typecourse'] = 'Course event';
7330   * </code>
7331   * If you want to display the string "Course event" in any language
7332   * supported you would use the identifier 'typecourse' and the module 'calendar'
7333   * (because it is in the file calendar.php):
7334   * <code>
7335   * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7336   * </code>
7337   *
7338   * As a last resort, should the identifier fail to map to a string
7339   * the returned string will be [[ $identifier ]]
7340   *
7341   * In Moodle 2.3 there is a new argument to this function $lazyload.
7342   * Setting $lazyload to true causes get_string to return a lang_string object
7343   * rather than the string itself. The fetching of the string is then put off until
7344   * the string object is first used. The object can be used by calling it's out
7345   * method or by casting the object to a string, either directly e.g.
7346   *     (string)$stringobject
7347   * or indirectly by using the string within another string or echoing it out e.g.
7348   *     echo $stringobject
7349   *     return "<p>{$stringobject}</p>";
7350   * It is worth noting that using $lazyload and attempting to use the string as an
7351   * array key will cause a fatal error as objects cannot be used as array keys.
7352   * But you should never do that anyway!
7353   * For more information {@link lang_string}
7354   *
7355   * @category string
7356   * @param string $identifier The key identifier for the localized string
7357   * @param string $component The module where the key identifier is stored,
7358   *      usually expressed as the filename in the language pack without the
7359   *      .php on the end but can also be written as mod/forum or grade/export/xls.
7360   *      If none is specified then moodle.php is used.
7361   * @param string|object|array|int $a An object, string or number that can be used
7362   *      within translation strings
7363   * @param bool $lazyload If set to true a string object is returned instead of
7364   *      the string itself. The string then isn't calculated until it is first used.
7365   * @return string The localized string.
7366   * @throws coding_exception
7367   */
7368  function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7369      global $CFG;
7370  
7371      // If the lazy load argument has been supplied return a lang_string object
7372      // instead.
7373      // We need to make sure it is true (and a bool) as you will see below there
7374      // used to be a forth argument at one point.
7375      if ($lazyload === true) {
7376          return new lang_string($identifier, $component, $a);
7377      }
7378  
7379      if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7380          throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7381      }
7382  
7383      // There is now a forth argument again, this time it is a boolean however so
7384      // we can still check for the old extralocations parameter.
7385      if (!is_bool($lazyload) && !empty($lazyload)) {
7386          debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7387      }
7388  
7389      if (strpos((string)$component, '/') !== false) {
7390          debugging('The module name you passed to get_string is the deprecated format ' .
7391                  'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7392          $componentpath = explode('/', $component);
7393  
7394          switch ($componentpath[0]) {
7395              case 'mod':
7396                  $component = $componentpath[1];
7397                  break;
7398              case 'blocks':
7399              case 'block':
7400                  $component = 'block_'.$componentpath[1];
7401                  break;
7402              case 'enrol':
7403                  $component = 'enrol_'.$componentpath[1];
7404                  break;
7405              case 'format':
7406                  $component = 'format_'.$componentpath[1];
7407                  break;
7408              case 'grade':
7409                  $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7410                  break;
7411          }
7412      }
7413  
7414      $result = get_string_manager()->get_string($identifier, $component, $a);
7415  
7416      // Debugging feature lets you display string identifier and component.
7417      if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7418          $result .= ' {' . $identifier . '/' . $component . '}';
7419      }
7420      return $result;
7421  }
7422  
7423  /**
7424   * Converts an array of strings to their localized value.
7425   *
7426   * @param array $array An array of strings
7427   * @param string $component The language module that these strings can be found in.
7428   * @return stdClass translated strings.
7429   */
7430  function get_strings($array, $component = '') {
7431      $string = new stdClass;
7432      foreach ($array as $item) {
7433          $string->$item = get_string($item, $component);
7434      }
7435      return $string;
7436  }
7437  
7438  /**
7439   * Prints out a translated string.
7440   *
7441   * Prints out a translated string using the return value from the {@link get_string()} function.
7442   *
7443   * Example usage of this function when the string is in the moodle.php file:<br/>
7444   * <code>
7445   * echo '<strong>';
7446   * print_string('course');
7447   * echo '</strong>';
7448   * </code>
7449   *
7450   * Example usage of this function when the string is not in the moodle.php file:<br/>
7451   * <code>
7452   * echo '<h1>';
7453   * print_string('typecourse', 'calendar');
7454   * echo '</h1>';
7455   * </code>
7456   *
7457   * @category string
7458   * @param string $identifier The key identifier for the localized string
7459   * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7460   * @param string|object|array $a An object, string or number that can be used within translation strings
7461   */
7462  function print_string($identifier, $component = '', $a = null) {
7463      echo get_string($identifier, $component, $a);
7464  }
7465  
7466  /**
7467   * Returns a list of charset codes
7468   *
7469   * Returns a list of charset codes. It's hardcoded, so they should be added manually
7470   * (checking that such charset is supported by the texlib library!)
7471   *
7472   * @return array And associative array with contents in the form of charset => charset
7473   */
7474  function get_list_of_charsets() {
7475  
7476      $charsets = array(
7477          'EUC-JP'     => 'EUC-JP',
7478          'ISO-2022-JP'=> 'ISO-2022-JP',
7479          'ISO-8859-1' => 'ISO-8859-1',
7480          'SHIFT-JIS'  => 'SHIFT-JIS',
7481          'GB2312'     => 'GB2312',
7482          'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7483          'UTF-8'      => 'UTF-8');
7484  
7485      asort($charsets);
7486  
7487      return $charsets;
7488  }
7489  
7490  /**
7491   * Returns a list of valid and compatible themes
7492   *
7493   * @return array
7494   */
7495  function get_list_of_themes() {
7496      global $CFG;
7497  
7498      $themes = array();
7499  
7500      if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7501          $themelist = explode(',', $CFG->themelist);
7502      } else {
7503          $themelist = array_keys(core_component::get_plugin_list("theme"));
7504      }
7505  
7506      foreach ($themelist as $key => $themename) {
7507          $theme = theme_config::load($themename);
7508          $themes[$themename] = $theme;
7509      }
7510  
7511      core_collator::asort_objects_by_method($themes, 'get_theme_name');
7512  
7513      return $themes;
7514  }
7515  
7516  /**
7517   * Factory function for emoticon_manager
7518   *
7519   * @return emoticon_manager singleton
7520   */
7521  function get_emoticon_manager() {
7522      static $singleton = null;
7523  
7524      if (is_null($singleton)) {
7525          $singleton = new emoticon_manager();
7526      }
7527  
7528      return $singleton;
7529  }
7530  
7531  /**
7532   * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7533   *
7534   * Whenever this manager mentiones 'emoticon object', the following data
7535   * structure is expected: stdClass with properties text, imagename, imagecomponent,
7536   * altidentifier and altcomponent
7537   *
7538   * @see admin_setting_emoticons
7539   *
7540   * @copyright 2010 David Mudrak
7541   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7542   */
7543  class emoticon_manager {
7544  
7545      /**
7546       * Returns the currently enabled emoticons
7547       *
7548       * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7549       * @return array of emoticon objects
7550       */
7551      public function get_emoticons($selectable = false) {
7552          global $CFG;
7553          $notselectable = ['martin', 'egg'];
7554  
7555          if (empty($CFG->emoticons)) {
7556              return array();
7557          }
7558  
7559          $emoticons = $this->decode_stored_config($CFG->emoticons);
7560  
7561          if (!is_array($emoticons)) {
7562              // Something is wrong with the format of stored setting.
7563              debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7564              return array();
7565          }
7566          if ($selectable) {
7567              foreach ($emoticons as $index => $emote) {
7568                  if (in_array($emote->altidentifier, $notselectable)) {
7569                      // Skip this one.
7570                      unset($emoticons[$index]);
7571                  }
7572              }
7573          }
7574  
7575          return $emoticons;
7576      }
7577  
7578      /**
7579       * Converts emoticon object into renderable pix_emoticon object
7580       *
7581       * @param stdClass $emoticon emoticon object
7582       * @param array $attributes explicit HTML attributes to set
7583       * @return pix_emoticon
7584       */
7585      public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7586          $stringmanager = get_string_manager();
7587          if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7588              $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7589          } else {
7590              $alt = s($emoticon->text);
7591          }
7592          return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7593      }
7594  
7595      /**
7596       * Encodes the array of emoticon objects into a string storable in config table
7597       *
7598       * @see self::decode_stored_config()
7599       * @param array $emoticons array of emtocion objects
7600       * @return string
7601       */
7602      public function encode_stored_config(array $emoticons) {
7603          return json_encode($emoticons);
7604      }
7605  
7606      /**
7607       * Decodes the string into an array of emoticon objects
7608       *
7609       * @see self::encode_stored_config()
7610       * @param string $encoded
7611       * @return array|null
7612       */
7613      public function decode_stored_config($encoded) {
7614          $decoded = json_decode($encoded);
7615          if (!is_array($decoded)) {
7616              return null;
7617          }
7618          return $decoded;
7619      }
7620  
7621      /**
7622       * Returns default set of emoticons supported by Moodle
7623       *
7624       * @return array of sdtClasses
7625       */
7626      public function default_emoticons() {
7627          return array(
7628              $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7629              $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7630              $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7631              $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7632              $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7633              $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7634              $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7635              $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7636              $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7637              $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7638              $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7639              $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7640              $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7641              $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7642              $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7643              $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7644              $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7645              $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7646              $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7647              $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7648              $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7649              $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7650              $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7651              $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7652              $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7653              $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7654              $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7655              $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7656              $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7657              $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7658          );
7659      }
7660  
7661      /**
7662       * Helper method preparing the stdClass with the emoticon properties
7663       *
7664       * @param string|array $text or array of strings
7665       * @param string $imagename to be used by {@link pix_emoticon}
7666       * @param string $altidentifier alternative string identifier, null for no alt
7667       * @param string $altcomponent where the alternative string is defined
7668       * @param string $imagecomponent to be used by {@link pix_emoticon}
7669       * @return stdClass
7670       */
7671      protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7672                                                 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7673          return (object)array(
7674              'text'           => $text,
7675              'imagename'      => $imagename,
7676              'imagecomponent' => $imagecomponent,
7677              'altidentifier'  => $altidentifier,
7678              'altcomponent'   => $altcomponent,
7679          );
7680      }
7681  }
7682  
7683  // ENCRYPTION.
7684  
7685  /**
7686   * rc4encrypt
7687   *
7688   * @param string $data        Data to encrypt.
7689   * @return string             The now encrypted data.
7690   */
7691  function rc4encrypt($data) {
7692      return endecrypt(get_site_identifier(), $data, '');
7693  }
7694  
7695  /**
7696   * rc4decrypt
7697   *
7698   * @param string $data        Data to decrypt.
7699   * @return string             The now decrypted data.
7700   */
7701  function rc4decrypt($data) {
7702      return endecrypt(get_site_identifier(), $data, 'de');
7703  }
7704  
7705  /**
7706   * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7707   *
7708   * @todo Finish documenting this function
7709   *
7710   * @param string $pwd The password to use when encrypting or decrypting
7711   * @param string $data The data to be decrypted/encrypted
7712   * @param string $case Either 'de' for decrypt or '' for encrypt
7713   * @return string
7714   */
7715  function endecrypt ($pwd, $data, $case) {
7716  
7717      if ($case == 'de') {
7718          $data = urldecode($data);
7719      }
7720  
7721      $key[] = '';
7722      $box[] = '';
7723      $pwdlength = strlen($pwd);
7724  
7725      for ($i = 0; $i <= 255; $i++) {
7726          $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7727          $box[$i] = $i;
7728      }
7729  
7730      $x = 0;
7731  
7732      for ($i = 0; $i <= 255; $i++) {
7733          $x = ($x + $box[$i] + $key[$i]) % 256;
7734          $tempswap = $box[$i];
7735          $box[$i] = $box[$x];
7736          $box[$x] = $tempswap;
7737      }
7738  
7739      $cipher = '';
7740  
7741      $a = 0;
7742      $j = 0;
7743  
7744      for ($i = 0; $i < strlen($data); $i++) {
7745          $a = ($a + 1) % 256;
7746          $j = ($j + $box[$a]) % 256;
7747          $temp = $box[$a];
7748          $box[$a] = $box[$j];
7749          $box[$j] = $temp;
7750          $k = $box[(($box[$a] + $box[$j]) % 256)];
7751          $cipherby = ord(substr($data, $i, 1)) ^ $k;
7752          $cipher .= chr($cipherby);
7753      }
7754  
7755      if ($case == 'de') {
7756          $cipher = urldecode(urlencode($cipher));
7757      } else {
7758          $cipher = urlencode($cipher);
7759      }
7760  
7761      return $cipher;
7762  }
7763  
7764  // ENVIRONMENT CHECKING.
7765  
7766  /**
7767   * This method validates a plug name. It is much faster than calling clean_param.
7768   *
7769   * @param string $name a string that might be a plugin name.
7770   * @return bool if this string is a valid plugin name.
7771   */
7772  function is_valid_plugin_name($name) {
7773      // This does not work for 'mod', bad luck, use any other type.
7774      return core_component::is_valid_plugin_name('tool', $name);
7775  }
7776  
7777  /**
7778   * Get a list of all the plugins of a given type that define a certain API function
7779   * in a certain file. The plugin component names and function names are returned.
7780   *
7781   * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7782   * @param string $function the part of the name of the function after the
7783   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7784   *      names like report_courselist_hook.
7785   * @param string $file the name of file within the plugin that defines the
7786   *      function. Defaults to lib.php.
7787   * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7788   *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7789   */
7790  function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7791      global $CFG;
7792  
7793      // We don't include here as all plugin types files would be included.
7794      $plugins = get_plugins_with_function($function, $file, false);
7795  
7796      if (empty($plugins[$plugintype])) {
7797          return array();
7798      }
7799  
7800      $allplugins = core_component::get_plugin_list($plugintype);
7801  
7802      // Reformat the array and include the files.
7803      $pluginfunctions = array();
7804      foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7805  
7806          // Check that it has not been removed and the file is still available.
7807          if (!empty($allplugins[$pluginname])) {
7808  
7809              $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7810              if (file_exists($filepath)) {
7811                  include_once($filepath);
7812  
7813                  // Now that the file is loaded, we must verify the function still exists.
7814                  if (function_exists($functionname)) {
7815                      $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7816                  } else {
7817                      // Invalidate the cache for next run.
7818                      \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7819                  }
7820              }
7821          }
7822      }
7823  
7824      return $pluginfunctions;
7825  }
7826  
7827  /**
7828   * Get a list of all the plugins that define a certain API function in a certain file.
7829   *
7830   * @param string $function the part of the name of the function after the
7831   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7832   *      names like report_courselist_hook.
7833   * @param string $file the name of file within the plugin that defines the
7834   *      function. Defaults to lib.php.
7835   * @param bool $include Whether to include the files that contain the functions or not.
7836   * @return array with [plugintype][plugin] = functionname
7837   */
7838  function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
7839      global $CFG;
7840  
7841      if (during_initial_install() || isset($CFG->upgraderunning)) {
7842          // API functions _must not_ be called during an installation or upgrade.
7843          return [];
7844      }
7845  
7846      $cache = \cache::make('core', 'plugin_functions');
7847  
7848      // Including both although I doubt that we will find two functions definitions with the same name.
7849      // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7850      $pluginfunctions = false;
7851      if (!empty($CFG->allversionshash)) {
7852          $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7853          $pluginfunctions = $cache->get($key);
7854      }
7855      $dirty = false;
7856  
7857      // Use the plugin manager to check that plugins are currently installed.
7858      $pluginmanager = \core_plugin_manager::instance();
7859  
7860      if ($pluginfunctions !== false) {
7861  
7862          // Checking that the files are still available.
7863          foreach ($pluginfunctions as $plugintype => $plugins) {
7864  
7865              $allplugins = \core_component::get_plugin_list($plugintype);
7866              $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7867              foreach ($plugins as $plugin => $function) {
7868                  if (!isset($installedplugins[$plugin])) {
7869                      // Plugin code is still present on disk but it is not installed.
7870                      $dirty = true;
7871                      break 2;
7872                  }
7873  
7874                  // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7875                  if (empty($allplugins[$plugin])) {
7876                      $dirty = true;
7877                      break 2;
7878                  }
7879  
7880                  $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7881                  if ($include && $fileexists) {
7882                      // Include the files if it was requested.
7883                      include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7884                  } else if (!$fileexists) {
7885                      // If the file is not available any more it should not be returned.
7886                      $dirty = true;
7887                      break 2;
7888                  }
7889  
7890                  // Check if the function still exists in the file.
7891                  if ($include && !function_exists($function)) {
7892                      $dirty = true;
7893                      break 2;
7894                  }
7895              }
7896          }
7897  
7898          // If the cache is dirty, we should fall through and let it rebuild.
7899          if (!$dirty) {
7900              return $pluginfunctions;
7901          }
7902      }
7903  
7904      $pluginfunctions = array();
7905  
7906      // To fill the cached. Also, everything should continue working with cache disabled.
7907      $plugintypes = \core_component::get_plugin_types();
7908      foreach ($plugintypes as $plugintype => $unused) {
7909  
7910          // We need to include files here.
7911          $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7912          $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7913          foreach ($pluginswithfile as $plugin => $notused) {
7914  
7915              if (!isset($installedplugins[$plugin])) {
7916                  continue;
7917              }
7918  
7919              $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7920  
7921              $pluginfunction = false;
7922              if (function_exists($fullfunction)) {
7923                  // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7924                  $pluginfunction = $fullfunction;
7925  
7926              } else if ($plugintype === 'mod') {
7927                  // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7928                  $shortfunction = $plugin . '_' . $function;
7929                  if (function_exists($shortfunction)) {
7930                      $pluginfunction = $shortfunction;
7931                  }
7932              }
7933  
7934              if ($pluginfunction) {
7935                  if (empty($pluginfunctions[$plugintype])) {
7936                      $pluginfunctions[$plugintype] = array();
7937                  }
7938                  $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7939              }
7940  
7941          }
7942      }
7943      if (!empty($CFG->allversionshash)) {
7944          $cache->set($key, $pluginfunctions);
7945      }
7946  
7947      return $pluginfunctions;
7948  
7949  }
7950  
7951  /**
7952   * Lists plugin-like directories within specified directory
7953   *
7954   * This function was originally used for standard Moodle plugins, please use
7955   * new core_component::get_plugin_list() now.
7956   *
7957   * This function is used for general directory listing and backwards compatility.
7958   *
7959   * @param string $directory relative directory from root
7960   * @param string $exclude dir name to exclude from the list (defaults to none)
7961   * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7962   * @return array Sorted array of directory names found under the requested parameters
7963   */
7964  function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7965      global $CFG;
7966  
7967      $plugins = array();
7968  
7969      if (empty($basedir)) {
7970          $basedir = $CFG->dirroot .'/'. $directory;
7971  
7972      } else {
7973          $basedir = $basedir .'/'. $directory;
7974      }
7975  
7976      if ($CFG->debugdeveloper and empty($exclude)) {
7977          // Make sure devs do not use this to list normal plugins,
7978          // this is intended for general directories that are not plugins!
7979  
7980          $subtypes = core_component::get_plugin_types();
7981          if (in_array($basedir, $subtypes)) {
7982              debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7983          }
7984          unset($subtypes);
7985      }
7986  
7987      $ignorelist = array_flip(array_filter([
7988          'CVS',
7989          '_vti_cnf',
7990          'amd',
7991          'classes',
7992          'simpletest',
7993          'tests',
7994          'templates',
7995          'yui',
7996          $exclude,
7997      ]));
7998  
7999      if (file_exists($basedir) && filetype($basedir) == 'dir') {
8000          if (!$dirhandle = opendir($basedir)) {
8001              debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8002              return array();
8003          }
8004          while (false !== ($dir = readdir($dirhandle))) {
8005              if (strpos($dir, '.') === 0) {
8006                  // Ignore directories starting with .
8007                  // These are treated as hidden directories.
8008                  continue;
8009              }
8010              if (array_key_exists($dir, $ignorelist)) {
8011                  // This directory features on the ignore list.
8012                  continue;
8013              }
8014              if (filetype($basedir .'/'. $dir) != 'dir') {
8015                  continue;
8016              }
8017              $plugins[] = $dir;
8018          }
8019          closedir($dirhandle);
8020      }
8021      if ($plugins) {
8022          asort($plugins);
8023      }
8024      return $plugins;
8025  }
8026  
8027  /**
8028   * Invoke plugin's callback functions
8029   *
8030   * @param string $type plugin type e.g. 'mod'
8031   * @param string $name plugin name
8032   * @param string $feature feature name
8033   * @param string $action feature's action
8034   * @param array $params parameters of callback function, should be an array
8035   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8036   * @return mixed
8037   *
8038   * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8039   */
8040  function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8041      return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8042  }
8043  
8044  /**
8045   * Invoke component's callback functions
8046   *
8047   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8048   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8049   * @param array $params parameters of callback function
8050   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8051   * @return mixed
8052   */
8053  function component_callback($component, $function, array $params = array(), $default = null) {
8054  
8055      $functionname = component_callback_exists($component, $function);
8056  
8057      if ($params && (array_keys($params) !== range(0, count($params) - 1))) {
8058          // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but
8059          // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions.
8060          // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array
8061          // This check can be removed when minimum PHP version for Moodle is raised to 8.
8062          debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.',
8063              DEBUG_DEVELOPER);
8064          $params = array_values($params);
8065      }
8066  
8067      if ($functionname) {
8068          // Function exists, so just return function result.
8069          $ret = call_user_func_array($functionname, $params);
8070          if (is_null($ret)) {
8071              return $default;
8072          } else {
8073              return $ret;
8074          }
8075      }
8076      return $default;
8077  }
8078  
8079  /**
8080   * Determine if a component callback exists and return the function name to call. Note that this
8081   * function will include the required library files so that the functioname returned can be
8082   * called directly.
8083   *
8084   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8085   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8086   * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8087   * @throws coding_exception if invalid component specfied
8088   */
8089  function component_callback_exists($component, $function) {
8090      global $CFG; // This is needed for the inclusions.
8091  
8092      $cleancomponent = clean_param($component, PARAM_COMPONENT);
8093      if (empty($cleancomponent)) {
8094          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8095      }
8096      $component = $cleancomponent;
8097  
8098      list($type, $name) = core_component::normalize_component($component);
8099      $component = $type . '_' . $name;
8100  
8101      $oldfunction = $name.'_'.$function;
8102      $function = $component.'_'.$function;
8103  
8104      $dir = core_component::get_component_directory($component);
8105      if (empty($dir)) {
8106          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8107      }
8108  
8109      // Load library and look for function.
8110      if (file_exists($dir.'/lib.php')) {
8111          require_once($dir.'/lib.php');
8112      }
8113  
8114      if (!function_exists($function) and function_exists($oldfunction)) {
8115          if ($type !== 'mod' and $type !== 'core') {
8116              debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8117          }
8118          $function = $oldfunction;
8119      }
8120  
8121      if (function_exists($function)) {
8122          return $function;
8123      }
8124      return false;
8125  }
8126  
8127  /**
8128   * Call the specified callback method on the provided class.
8129   *
8130   * If the callback returns null, then the default value is returned instead.
8131   * If the class does not exist, then the default value is returned.
8132   *
8133   * @param   string      $classname The name of the class to call upon.
8134   * @param   string      $methodname The name of the staticically defined method on the class.
8135   * @param   array       $params The arguments to pass into the method.
8136   * @param   mixed       $default The default value.
8137   * @return  mixed       The return value.
8138   */
8139  function component_class_callback($classname, $methodname, array $params, $default = null) {
8140      if (!class_exists($classname)) {
8141          return $default;
8142      }
8143  
8144      if (!method_exists($classname, $methodname)) {
8145          return $default;
8146      }
8147  
8148      $fullfunction = $classname . '::' . $methodname;
8149      $result = call_user_func_array($fullfunction, $params);
8150  
8151      if (null === $result) {
8152          return $default;
8153      } else {
8154          return $result;
8155      }
8156  }
8157  
8158  /**
8159   * Checks whether a plugin supports a specified feature.
8160   *
8161   * @param string $type Plugin type e.g. 'mod'
8162   * @param string $name Plugin name e.g. 'forum'
8163   * @param string $feature Feature code (FEATURE_xx constant)
8164   * @param mixed $default default value if feature support unknown
8165   * @return mixed Feature result (false if not supported, null if feature is unknown,
8166   *         otherwise usually true but may have other feature-specific value such as array)
8167   * @throws coding_exception
8168   */
8169  function plugin_supports($type, $name, $feature, $default = null) {
8170      global $CFG;
8171  
8172      if ($type === 'mod' and $name === 'NEWMODULE') {
8173          // Somebody forgot to rename the module template.
8174          return false;
8175      }
8176  
8177      $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8178      if (empty($component)) {
8179          throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8180      }
8181  
8182      $function = null;
8183  
8184      if ($type === 'mod') {
8185          // We need this special case because we support subplugins in modules,
8186          // otherwise it would end up in infinite loop.
8187          if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8188              include_once("$CFG->dirroot/mod/$name/lib.php");
8189              $function = $component.'_supports';
8190              if (!function_exists($function)) {
8191                  // Legacy non-frankenstyle function name.
8192                  $function = $name.'_supports';
8193              }
8194          }
8195  
8196      } else {
8197          if (!$path = core_component::get_plugin_directory($type, $name)) {
8198              // Non existent plugin type.
8199              return false;
8200          }
8201          if (file_exists("$path/lib.php")) {
8202              include_once("$path/lib.php");
8203              $function = $component.'_supports';
8204          }
8205      }
8206  
8207      if ($function and function_exists($function)) {
8208          $supports = $function($feature);
8209          if (is_null($supports)) {
8210              // Plugin does not know - use default.
8211              return $default;
8212          } else {
8213              return $supports;
8214          }
8215      }
8216  
8217      // Plugin does not care, so use default.
8218      return $default;
8219  }
8220  
8221  /**
8222   * Returns true if the current version of PHP is greater that the specified one.
8223   *
8224   * @todo Check PHP version being required here is it too low?
8225   *
8226   * @param string $version The version of php being tested.
8227   * @return bool
8228   */
8229  function check_php_version($version='5.2.4') {
8230      return (version_compare(phpversion(), $version) >= 0);
8231  }
8232  
8233  /**
8234   * Determine if moodle installation requires update.
8235   *
8236   * Checks version numbers of main code and all plugins to see
8237   * if there are any mismatches.
8238   *
8239   * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8240   * @return bool
8241   */
8242  function moodle_needs_upgrading($checkupgradeflag = true) {
8243      global $CFG, $DB;
8244  
8245      // Say no if there is already an upgrade running.
8246      if ($checkupgradeflag) {
8247          $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8248          $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
8249          // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8250          // We should always return false.
8251          // This means the upgrade is running from CLI somewhere, or about to.
8252          if (!empty($lock) && !$currentprocessrunningupgrade) {
8253              return false;
8254          }
8255      }
8256  
8257      if (empty($CFG->version)) {
8258          return true;
8259      }
8260  
8261      // There is no need to purge plugininfo caches here because
8262      // these caches are not used during upgrade and they are purged after
8263      // every upgrade.
8264  
8265      if (empty($CFG->allversionshash)) {
8266          return true;
8267      }
8268  
8269      $hash = core_component::get_all_versions_hash();
8270  
8271      return ($hash !== $CFG->allversionshash);
8272  }
8273  
8274  /**
8275   * Returns the major version of this site
8276   *
8277   * Moodle version numbers consist of three numbers separated by a dot, for
8278   * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8279   * called major version. This function extracts the major version from either
8280   * $CFG->release (default) or eventually from the $release variable defined in
8281   * the main version.php.
8282   *
8283   * @param bool $fromdisk should the version if source code files be used
8284   * @return string|false the major version like '2.3', false if could not be determined
8285   */
8286  function moodle_major_version($fromdisk = false) {
8287      global $CFG;
8288  
8289      if ($fromdisk) {
8290          $release = null;
8291          require($CFG->dirroot.'/version.php');
8292          if (empty($release)) {
8293              return false;
8294          }
8295  
8296      } else {
8297          if (empty($CFG->release)) {
8298              return false;
8299          }
8300          $release = $CFG->release;
8301      }
8302  
8303      if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8304          return $matches[0];
8305      } else {
8306          return false;
8307      }
8308  }
8309  
8310  // MISCELLANEOUS.
8311  
8312  /**
8313   * Gets the system locale
8314   *
8315   * @return string Retuns the current locale.
8316   */
8317  function moodle_getlocale() {
8318      global $CFG;
8319  
8320      // Fetch the correct locale based on ostype.
8321      if ($CFG->ostype == 'WINDOWS') {
8322          $stringtofetch = 'localewin';
8323      } else {
8324          $stringtofetch = 'locale';
8325      }
8326  
8327      if (!empty($CFG->locale)) { // Override locale for all language packs.
8328          return $CFG->locale;
8329      }
8330  
8331      return get_string($stringtofetch, 'langconfig');
8332  }
8333  
8334  /**
8335   * Sets the system locale
8336   *
8337   * @category string
8338   * @param string $locale Can be used to force a locale
8339   */
8340  function moodle_setlocale($locale='') {
8341      global $CFG;
8342  
8343      static $currentlocale = ''; // Last locale caching.
8344  
8345      $oldlocale = $currentlocale;
8346  
8347      // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8348      if (!empty($locale)) {
8349          $currentlocale = $locale;
8350      } else {
8351          $currentlocale = moodle_getlocale();
8352      }
8353  
8354      // Do nothing if locale already set up.
8355      if ($oldlocale == $currentlocale) {
8356          return;
8357      }
8358  
8359      // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8360      // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8361      // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8362  
8363      // Get current values.
8364      $monetary= setlocale (LC_MONETARY, 0);
8365      $numeric = setlocale (LC_NUMERIC, 0);
8366      $ctype   = setlocale (LC_CTYPE, 0);
8367      if ($CFG->ostype != 'WINDOWS') {
8368          $messages= setlocale (LC_MESSAGES, 0);
8369      }
8370      // Set locale to all.
8371      $result = setlocale (LC_ALL, $currentlocale);
8372      // If setting of locale fails try the other utf8 or utf-8 variant,
8373      // some operating systems support both (Debian), others just one (OSX).
8374      if ($result === false) {
8375          if (stripos($currentlocale, '.UTF-8') !== false) {
8376              $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8377              setlocale (LC_ALL, $newlocale);
8378          } else if (stripos($currentlocale, '.UTF8') !== false) {
8379              $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8380              setlocale (LC_ALL, $newlocale);
8381          }
8382      }
8383      // Set old values.
8384      setlocale (LC_MONETARY, $monetary);
8385      setlocale (LC_NUMERIC, $numeric);
8386      if ($CFG->ostype != 'WINDOWS') {
8387          setlocale (LC_MESSAGES, $messages);
8388      }
8389      if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8390          // To workaround a well-known PHP problem with Turkish letter Ii.
8391          setlocale (LC_CTYPE, $ctype);
8392      }
8393  }
8394  
8395  /**
8396   * Count words in a string.
8397   *
8398   * Words are defined as things between whitespace.
8399   *
8400   * @category string
8401   * @param string $string The text to be searched for words. May be HTML.
8402   * @param int|null $format
8403   * @return int The count of words in the specified string
8404   */
8405  function count_words($string, $format = null) {
8406      // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8407      // Also, br is a special case because it definitely delimits a word, but has no close tag.
8408      $string = preg_replace('~
8409              (                                   # Capture the tag we match.
8410                  </                              # Start of close tag.
8411                  (?!                             # Do not match any of these specific close tag names.
8412                      a> | b> | del> | em> | i> |
8413                      ins> | s> | small> | span> |
8414                      strong> | sub> | sup> | u>
8415                  )
8416                  \w+                             # But, apart from those execptions, match any tag name.
8417                  >                               # End of close tag.
8418              |
8419                  <br> | <br\s*/>                 # Special cases that are not close tags.
8420              )
8421              ~x', '$1 ', $string); // Add a space after the close tag.
8422      if ($format !== null && $format != FORMAT_PLAIN) {
8423          // Match the usual text cleaning before display.
8424          // Ideally we should apply multilang filter only here, other filters might add extra text.
8425          $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8426      }
8427      // Now remove HTML tags.
8428      $string = strip_tags($string);
8429      // Decode HTML entities.
8430      $string = html_entity_decode($string, ENT_COMPAT);
8431  
8432      // Now, the word count is the number of blocks of characters separated
8433      // by any sort of space. That seems to be the definition used by all other systems.
8434      // To be precise about what is considered to separate words:
8435      // * Anything that Unicode considers a 'Separator'
8436      // * Anything that Unicode considers a 'Control character'
8437      // * An em- or en- dash.
8438      return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8439  }
8440  
8441  /**
8442   * Count letters in a string.
8443   *
8444   * Letters are defined as chars not in tags and different from whitespace.
8445   *
8446   * @category string
8447   * @param string $string The text to be searched for letters. May be HTML.
8448   * @param int|null $format
8449   * @return int The count of letters in the specified text.
8450   */
8451  function count_letters($string, $format = null) {
8452      if ($format !== null && $format != FORMAT_PLAIN) {
8453          // Match the usual text cleaning before display.
8454          // Ideally we should apply multilang filter only here, other filters might add extra text.
8455          $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8456      }
8457      $string = strip_tags($string); // Tags are out now.
8458      $string = html_entity_decode($string, ENT_COMPAT);
8459      $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8460  
8461      return core_text::strlen($string);
8462  }
8463  
8464  /**
8465   * Generate and return a random string of the specified length.
8466   *
8467   * @param int $length The length of the string to be created.
8468   * @return string
8469   */
8470  function random_string($length=15) {
8471      $randombytes = random_bytes_emulate($length);
8472      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8473      $pool .= 'abcdefghijklmnopqrstuvwxyz';
8474      $pool .= '0123456789';
8475      $poollen = strlen($pool);
8476      $string = '';
8477      for ($i = 0; $i < $length; $i++) {
8478          $rand = ord($randombytes[$i]);
8479          $string .= substr($pool, ($rand%($poollen)), 1);
8480      }
8481      return $string;
8482  }
8483  
8484  /**
8485   * Generate a complex random string (useful for md5 salts)
8486   *
8487   * This function is based on the above {@link random_string()} however it uses a
8488   * larger pool of characters and generates a string between 24 and 32 characters
8489   *
8490   * @param int $length Optional if set generates a string to exactly this length
8491   * @return string
8492   */
8493  function complex_random_string($length=null) {
8494      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8495      $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8496      $poollen = strlen($pool);
8497      if ($length===null) {
8498          $length = floor(rand(24, 32));
8499      }
8500      $randombytes = random_bytes_emulate($length);
8501      $string = '';
8502      for ($i = 0; $i < $length; $i++) {
8503          $rand = ord($randombytes[$i]);
8504          $string .= $pool[($rand%$poollen)];
8505      }
8506      return $string;
8507  }
8508  
8509  /**
8510   * Try to generates cryptographically secure pseudo-random bytes.
8511   *
8512   * Note this is achieved by fallbacking between:
8513   *  - PHP 7 random_bytes().
8514   *  - OpenSSL openssl_random_pseudo_bytes().
8515   *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
8516   *
8517   * @param int $length requested length in bytes
8518   * @return string binary data
8519   */
8520  function random_bytes_emulate($length) {
8521      global $CFG;
8522      if ($length <= 0) {
8523          debugging('Invalid random bytes length', DEBUG_DEVELOPER);
8524          return '';
8525      }
8526      if (function_exists('random_bytes')) {
8527          // Use PHP 7 goodness.
8528          $hash = @random_bytes($length);
8529          if ($hash !== false) {
8530              return $hash;
8531          }
8532      }
8533      if (function_exists('openssl_random_pseudo_bytes')) {
8534          // If you have the openssl extension enabled.
8535          $hash = openssl_random_pseudo_bytes($length);
8536          if ($hash !== false) {
8537              return $hash;
8538          }
8539      }
8540  
8541      // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess.
8542      $staticdata = serialize($CFG) . serialize($_SERVER);
8543      $hash = '';
8544      do {
8545          $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true);
8546      } while (strlen($hash) < $length);
8547  
8548      return substr($hash, 0, $length);
8549  }
8550  
8551  /**
8552   * Given some text (which may contain HTML) and an ideal length,
8553   * this function truncates the text neatly on a word boundary if possible
8554   *
8555   * @category string
8556   * @param string $text text to be shortened
8557   * @param int $ideal ideal string length
8558   * @param boolean $exact if false, $text will not be cut mid-word
8559   * @param string $ending The string to append if the passed string is truncated
8560   * @return string $truncate shortened string
8561   */
8562  function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8563      // If the plain text is shorter than the maximum length, return the whole text.
8564      if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8565          return $text;
8566      }
8567  
8568      // Splits on HTML tags. Each open/close/empty tag will be the first thing
8569      // and only tag in its 'line'.
8570      preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8571  
8572      $totallength = core_text::strlen($ending);
8573      $truncate = '';
8574  
8575      // This array stores information about open and close tags and their position
8576      // in the truncated string. Each item in the array is an object with fields
8577      // ->open (true if open), ->tag (tag name in lower case), and ->pos
8578      // (byte position in truncated text).
8579      $tagdetails = array();
8580  
8581      foreach ($lines as $linematchings) {
8582          // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8583          if (!empty($linematchings[1])) {
8584              // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8585              if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8586                  if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8587                      // Record closing tag.
8588                      $tagdetails[] = (object) array(
8589                              'open' => false,
8590                              'tag'  => core_text::strtolower($tagmatchings[1]),
8591                              'pos'  => core_text::strlen($truncate),
8592                          );
8593  
8594                  } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8595                      // Record opening tag.
8596                      $tagdetails[] = (object) array(
8597                              'open' => true,
8598                              'tag'  => core_text::strtolower($tagmatchings[1]),
8599                              'pos'  => core_text::strlen($truncate),
8600                          );
8601                  } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8602                      $tagdetails[] = (object) array(
8603                              'open' => true,
8604                              'tag'  => core_text::strtolower('if'),
8605                              'pos'  => core_text::strlen($truncate),
8606                      );
8607                  } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8608                      $tagdetails[] = (object) array(
8609                              'open' => false,
8610                              'tag'  => core_text::strtolower('if'),
8611                              'pos'  => core_text::strlen($truncate),
8612                      );
8613                  }
8614              }
8615              // Add html-tag to $truncate'd text.
8616              $truncate .= $linematchings[1];
8617          }
8618  
8619          // Calculate the length of the plain text part of the line; handle entities as one character.
8620          $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8621          if ($totallength + $contentlength > $ideal) {
8622              // The number of characters which are left.
8623              $left = $ideal - $totallength;
8624              $entitieslength = 0;
8625              // Search for html entities.
8626              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)) {
8627                  // Calculate the real length of all entities in the legal range.
8628                  foreach ($entities[0] as $entity) {
8629                      if ($entity[1]+1-$entitieslength <= $left) {
8630                          $left--;
8631                          $entitieslength += core_text::strlen($entity[0]);
8632                      } else {
8633                          // No more characters left.
8634                          break;
8635                      }
8636                  }
8637              }
8638              $breakpos = $left + $entitieslength;
8639  
8640              // If the words shouldn't be cut in the middle...
8641              if (!$exact) {
8642                  // Search the last occurence of a space.
8643                  for (; $breakpos > 0; $breakpos--) {
8644                      if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8645                          if ($char === '.' or $char === ' ') {
8646                              $breakpos += 1;
8647                              break;
8648                          } else if (strlen($char) > 2) {
8649                              // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8650                              $breakpos += 1;
8651                              break;
8652                          }
8653                      }
8654                  }
8655              }
8656              if ($breakpos == 0) {
8657                  // This deals with the test_shorten_text_no_spaces case.
8658                  $breakpos = $left + $entitieslength;
8659              } else if ($breakpos > $left + $entitieslength) {
8660                  // This deals with the previous for loop breaking on the first char.
8661                  $breakpos = $left + $entitieslength;
8662              }
8663  
8664              $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8665              // Maximum length is reached, so get off the loop.
8666              break;
8667          } else {
8668              $truncate .= $linematchings[2];
8669              $totallength += $contentlength;
8670          }
8671  
8672          // If the maximum length is reached, get off the loop.
8673          if ($totallength >= $ideal) {
8674              break;
8675          }
8676      }
8677  
8678      // Add the defined ending to the text.
8679      $truncate .= $ending;
8680  
8681      // Now calculate the list of open html tags based on the truncate position.
8682      $opentags = array();
8683      foreach ($tagdetails as $taginfo) {
8684          if ($taginfo->open) {
8685              // Add tag to the beginning of $opentags list.
8686              array_unshift($opentags, $taginfo->tag);
8687          } else {
8688              // Can have multiple exact same open tags, close the last one.
8689              $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8690              if ($pos !== false) {
8691                  unset($opentags[$pos]);
8692              }
8693          }
8694      }
8695  
8696      // Close all unclosed html-tags.
8697      foreach ($opentags as $tag) {
8698          if ($tag === 'if') {
8699              $truncate .= '<!--<![endif]-->';
8700          } else {
8701              $truncate .= '</' . $tag . '>';
8702          }
8703      }
8704  
8705      return $truncate;
8706  }
8707  
8708  /**
8709   * Shortens a given filename by removing characters positioned after the ideal string length.
8710   * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8711   * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8712   *
8713   * @param string $filename file name
8714   * @param int $length ideal string length
8715   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8716   * @return string $shortened shortened file name
8717   */
8718  function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8719      $shortened = $filename;
8720      // Extract a part of the filename if it's char size exceeds the ideal string length.
8721      if (core_text::strlen($filename) > $length) {
8722          // Exclude extension if present in filename.
8723          $mimetypes = get_mimetypes_array();
8724          $extension = pathinfo($filename, PATHINFO_EXTENSION);
8725          if ($extension && !empty($mimetypes[$extension])) {
8726              $basename = pathinfo($filename, PATHINFO_FILENAME);
8727              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8728              $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8729              $shortened .= '.' . $extension;
8730          } else {
8731              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8732              $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8733          }
8734      }
8735      return $shortened;
8736  }
8737  
8738  /**
8739   * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8740   *
8741   * @param array $path The paths to reduce the length.
8742   * @param int $length Ideal string length
8743   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8744   * @return array $result Shortened paths in array.
8745   */
8746  function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8747      $result = null;
8748  
8749      $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8750          $carry[] = shorten_filename($singlepath, $length, $includehash);
8751          return $carry;
8752      }, []);
8753  
8754      return $result;
8755  }
8756  
8757  /**
8758   * Given dates in seconds, how many weeks is the date from startdate
8759   * The first week is 1, the second 2 etc ...
8760   *
8761   * @param int $startdate Timestamp for the start date
8762   * @param int $thedate Timestamp for the end date
8763   * @return string
8764   */
8765  function getweek ($startdate, $thedate) {
8766      if ($thedate < $startdate) {
8767          return 0;
8768      }
8769  
8770      return floor(($thedate - $startdate) / WEEKSECS) + 1;
8771  }
8772  
8773  /**
8774   * Returns a randomly generated password of length $maxlen.  inspired by
8775   *
8776   * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8777   * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8778   *
8779   * @param int $maxlen  The maximum size of the password being generated.
8780   * @return string
8781   */
8782  function generate_password($maxlen=10) {
8783      global $CFG;
8784  
8785      if (empty($CFG->passwordpolicy)) {
8786          $fillers = PASSWORD_DIGITS;
8787          $wordlist = file($CFG->wordlist);
8788          $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8789          $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8790          $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8791          $password = $word1 . $filler1 . $word2;
8792      } else {
8793          $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8794          $digits = $CFG->minpassworddigits;
8795          $lower = $CFG->minpasswordlower;
8796          $upper = $CFG->minpasswordupper;
8797          $nonalphanum = $CFG->minpasswordnonalphanum;
8798          $total = $lower + $upper + $digits + $nonalphanum;
8799          // Var minlength should be the greater one of the two ( $minlen and $total ).
8800          $minlen = $minlen < $total ? $total : $minlen;
8801          // Var maxlen can never be smaller than minlen.
8802          $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8803          $additional = $maxlen - $total;
8804  
8805          // Make sure we have enough characters to fulfill
8806          // complexity requirements.
8807          $passworddigits = PASSWORD_DIGITS;
8808          while ($digits > strlen($passworddigits)) {
8809              $passworddigits .= PASSWORD_DIGITS;
8810          }
8811          $passwordlower = PASSWORD_LOWER;
8812          while ($lower > strlen($passwordlower)) {
8813              $passwordlower .= PASSWORD_LOWER;
8814          }
8815          $passwordupper = PASSWORD_UPPER;
8816          while ($upper > strlen($passwordupper)) {
8817              $passwordupper .= PASSWORD_UPPER;
8818          }
8819          $passwordnonalphanum = PASSWORD_NONALPHANUM;
8820          while ($nonalphanum > strlen($passwordnonalphanum)) {
8821              $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8822          }
8823  
8824          // Now mix and shuffle it all.
8825          $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8826                                   substr(str_shuffle ($passwordupper), 0, $upper) .
8827                                   substr(str_shuffle ($passworddigits), 0, $digits) .
8828                                   substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8829                                   substr(str_shuffle ($passwordlower .
8830                                                       $passwordupper .
8831                                                       $passworddigits .
8832                                                       $passwordnonalphanum), 0 , $additional));
8833      }
8834  
8835      return substr ($password, 0, $maxlen);
8836  }
8837  
8838  /**
8839   * Given a float, prints it nicely.
8840   * Localized floats must not be used in calculations!
8841   *
8842   * The stripzeros feature is intended for making numbers look nicer in small
8843   * areas where it is not necessary to indicate the degree of accuracy by showing
8844   * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8845   * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8846   *
8847   * @param float $float The float to print
8848   * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8849   * @param bool $localized use localized decimal separator
8850   * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8851   *                         the decimal point are always striped if $decimalpoints is -1.
8852   * @return string locale float
8853   */
8854  function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8855      if (is_null($float)) {
8856          return '';
8857      }
8858      if ($localized) {
8859          $separator = get_string('decsep', 'langconfig');
8860      } else {
8861          $separator = '.';
8862      }
8863      if ($decimalpoints == -1) {
8864          // The following counts the number of decimals.
8865          // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8866          $floatval = floatval($float);
8867          for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8868      }
8869  
8870      $result = number_format($float, $decimalpoints, $separator, '');
8871      if ($stripzeros && $decimalpoints > 0) {
8872          // Remove zeros and final dot if not needed.
8873          // However, only do this if there is a decimal point!
8874          $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8875      }
8876      return $result;
8877  }
8878  
8879  /**
8880   * Converts locale specific floating point/comma number back to standard PHP float value
8881   * Do NOT try to do any math operations before this conversion on any user submitted floats!
8882   *
8883   * @param string $localefloat locale aware float representation
8884   * @param bool $strict If true, then check the input and return false if it is not a valid number.
8885   * @return mixed float|bool - false or the parsed float.
8886   */
8887  function unformat_float($localefloat, $strict = false) {
8888      $localefloat = trim((string)$localefloat);
8889  
8890      if ($localefloat == '') {
8891          return null;
8892      }
8893  
8894      $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8895      $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8896  
8897      if ($strict && !is_numeric($localefloat)) {
8898          return false;
8899      }
8900  
8901      return (float)$localefloat;
8902  }
8903  
8904  /**
8905   * Given a simple array, this shuffles it up just like shuffle()
8906   * Unlike PHP's shuffle() this function works on any machine.
8907   *
8908   * @param array $array The array to be rearranged
8909   * @return array
8910   */
8911  function swapshuffle($array) {
8912  
8913      $last = count($array) - 1;
8914      for ($i = 0; $i <= $last; $i++) {
8915          $from = rand(0, $last);
8916          $curr = $array[$i];
8917          $array[$i] = $array[$from];
8918          $array[$from] = $curr;
8919      }
8920      return $array;
8921  }
8922  
8923  /**
8924   * Like {@link swapshuffle()}, but works on associative arrays
8925   *
8926   * @param array $array The associative array to be rearranged
8927   * @return array
8928   */
8929  function swapshuffle_assoc($array) {
8930  
8931      $newarray = array();
8932      $newkeys = swapshuffle(array_keys($array));
8933  
8934      foreach ($newkeys as $newkey) {
8935          $newarray[$newkey] = $array[$newkey];
8936      }
8937      return $newarray;
8938  }
8939  
8940  /**
8941   * Given an arbitrary array, and a number of draws,
8942   * this function returns an array with that amount
8943   * of items.  The indexes are retained.
8944   *
8945   * @todo Finish documenting this function
8946   *
8947   * @param array $array
8948   * @param int $draws
8949   * @return array
8950   */
8951  function draw_rand_array($array, $draws) {
8952  
8953      $return = array();
8954  
8955      $last = count($array);
8956  
8957      if ($draws > $last) {
8958          $draws = $last;
8959      }
8960  
8961      while ($draws > 0) {
8962          $last--;
8963  
8964          $keys = array_keys($array);
8965          $rand = rand(0, $last);
8966  
8967          $return[$keys[$rand]] = $array[$keys[$rand]];
8968          unset($array[$keys[$rand]]);
8969  
8970          $draws--;
8971      }
8972  
8973      return $return;
8974  }
8975  
8976  /**
8977   * Calculate the difference between two microtimes
8978   *
8979   * @param string $a The first Microtime
8980   * @param string $b The second Microtime
8981   * @return string
8982   */
8983  function microtime_diff($a, $b) {
8984      list($adec, $asec) = explode(' ', $a);
8985      list($bdec, $bsec) = explode(' ', $b);
8986      return $bsec - $asec + $bdec - $adec;
8987  }
8988  
8989  /**
8990   * Given a list (eg a,b,c,d,e) this function returns
8991   * an array of 1->a, 2->b, 3->c etc
8992   *
8993   * @param string $list The string to explode into array bits
8994   * @param string $separator The separator used within the list string
8995   * @return array The now assembled array
8996   */
8997  function make_menu_from_list($list, $separator=',') {
8998  
8999      $array = array_reverse(explode($separator, $list), true);
9000      foreach ($array as $key => $item) {
9001          $outarray[$key+1] = trim($item);
9002      }
9003      return $outarray;
9004  }
9005  
9006  /**
9007   * Creates an array that represents all the current grades that
9008   * can be chosen using the given grading type.
9009   *
9010   * Negative numbers
9011   * are scales, zero is no grade, and positive numbers are maximum
9012   * grades.
9013   *
9014   * @todo Finish documenting this function or better deprecated this completely!
9015   *
9016   * @param int $gradingtype
9017   * @return array
9018   */
9019  function make_grades_menu($gradingtype) {
9020      global $DB;
9021  
9022      $grades = array();
9023      if ($gradingtype < 0) {
9024          if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9025              return make_menu_from_list($scale->scale);
9026          }
9027      } else if ($gradingtype > 0) {
9028          for ($i=$gradingtype; $i>=0; $i--) {
9029              $grades[$i] = $i .' / '. $gradingtype;
9030          }
9031          return $grades;
9032      }
9033      return $grades;
9034  }
9035  
9036  /**
9037   * make_unique_id_code
9038   *
9039   * @todo Finish documenting this function
9040   *
9041   * @uses $_SERVER
9042   * @param string $extra Extra string to append to the end of the code
9043   * @return string
9044   */
9045  function make_unique_id_code($extra = '') {
9046  
9047      $hostname = 'unknownhost';
9048      if (!empty($_SERVER['HTTP_HOST'])) {
9049          $hostname = $_SERVER['HTTP_HOST'];
9050      } else if (!empty($_ENV['HTTP_HOST'])) {
9051          $hostname = $_ENV['HTTP_HOST'];
9052      } else if (!empty($_SERVER['SERVER_NAME'])) {
9053          $hostname = $_SERVER['SERVER_NAME'];
9054      } else if (!empty($_ENV['SERVER_NAME'])) {
9055          $hostname = $_ENV['SERVER_NAME'];
9056      }
9057  
9058      $date = gmdate("ymdHis");
9059  
9060      $random =  random_string(6);
9061  
9062      if ($extra) {
9063          return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9064      } else {
9065          return $hostname .'+'. $date .'+'. $random;
9066      }
9067  }
9068  
9069  
9070  /**
9071   * Function to check the passed address is within the passed subnet
9072   *
9073   * The parameter is a comma separated string of subnet definitions.
9074   * Subnet strings can be in one of three formats:
9075   *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
9076   *   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)
9077   *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
9078   * Code for type 1 modified from user posted comments by mediator at
9079   * {@link http://au.php.net/manual/en/function.ip2long.php}
9080   *
9081   * @param string $addr    The address you are checking
9082   * @param string $subnetstr    The string of subnet addresses
9083   * @param bool $checkallzeros    The state to whether check for 0.0.0.0
9084   * @return bool
9085   */
9086  function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
9087  
9088      if ($addr == '0.0.0.0' && !$checkallzeros) {
9089          return false;
9090      }
9091      $subnets = explode(',', $subnetstr);
9092      $found = false;
9093      $addr = trim($addr);
9094      $addr = cleanremoteaddr($addr, false); // Normalise.
9095      if ($addr === null) {
9096          return false;
9097      }
9098      $addrparts = explode(':', $addr);
9099  
9100      $ipv6 = strpos($addr, ':');
9101  
9102      foreach ($subnets as $subnet) {
9103          $subnet = trim($subnet);
9104          if ($subnet === '') {
9105              continue;
9106          }
9107  
9108          if (strpos($subnet, '/') !== false) {
9109              // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9110              list($ip, $mask) = explode('/', $subnet);
9111              $mask = trim($mask);
9112              if (!is_number($mask)) {
9113                  continue; // Incorect mask number, eh?
9114              }
9115              $ip = cleanremoteaddr($ip, false); // Normalise.
9116              if ($ip === null) {
9117                  continue;
9118              }
9119              if (strpos($ip, ':') !== false) {
9120                  // IPv6.
9121                  if (!$ipv6) {
9122                      continue;
9123                  }
9124                  if ($mask > 128 or $mask < 0) {
9125                      continue; // Nonsense.
9126                  }
9127                  if ($mask == 0) {
9128                      return true; // Any address.
9129                  }
9130                  if ($mask == 128) {
9131                      if ($ip === $addr) {
9132                          return true;
9133                      }
9134                      continue;
9135                  }
9136                  $ipparts = explode(':', $ip);
9137                  $modulo  = $mask % 16;
9138                  $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
9139                  $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9140                  if (implode(':', $ipnet) === implode(':', $addrnet)) {
9141                      if ($modulo == 0) {
9142                          return true;
9143                      }
9144                      $pos     = ($mask-$modulo)/16;
9145                      $ipnet   = hexdec($ipparts[$pos]);
9146                      $addrnet = hexdec($addrparts[$pos]);
9147                      $mask    = 0xffff << (16 - $modulo);
9148                      if (($addrnet & $mask) == ($ipnet & $mask)) {
9149                          return true;
9150                      }
9151                  }
9152  
9153              } else {
9154                  // IPv4.
9155                  if ($ipv6) {
9156                      continue;
9157                  }
9158                  if ($mask > 32 or $mask < 0) {
9159                      continue; // Nonsense.
9160                  }
9161                  if ($mask == 0) {
9162                      return true;
9163                  }
9164                  if ($mask == 32) {
9165                      if ($ip === $addr) {
9166                          return true;
9167                      }
9168                      continue;
9169                  }
9170                  $mask = 0xffffffff << (32 - $mask);
9171                  if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9172                      return true;
9173                  }
9174              }
9175  
9176          } else if (strpos($subnet, '-') !== false) {
9177              // 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.
9178              $parts = explode('-', $subnet);
9179              if (count($parts) != 2) {
9180                  continue;
9181              }
9182  
9183              if (strpos($subnet, ':') !== false) {
9184                  // IPv6.
9185                  if (!$ipv6) {
9186                      continue;
9187                  }
9188                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9189                  if ($ipstart === null) {
9190                      continue;
9191                  }
9192                  $ipparts = explode(':', $ipstart);
9193                  $start = hexdec(array_pop($ipparts));
9194                  $ipparts[] = trim($parts[1]);
9195                  $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9196                  if ($ipend === null) {
9197                      continue;
9198                  }
9199                  $ipparts[7] = '';
9200                  $ipnet = implode(':', $ipparts);
9201                  if (strpos($addr, $ipnet) !== 0) {
9202                      continue;
9203                  }
9204                  $ipparts = explode(':', $ipend);
9205                  $end = hexdec($ipparts[7]);
9206  
9207                  $addrend = hexdec($addrparts[7]);
9208  
9209                  if (($addrend >= $start) and ($addrend <= $end)) {
9210                      return true;
9211                  }
9212  
9213              } else {
9214                  // IPv4.
9215                  if ($ipv6) {
9216                      continue;
9217                  }
9218                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9219                  if ($ipstart === null) {
9220                      continue;
9221                  }
9222                  $ipparts = explode('.', $ipstart);
9223                  $ipparts[3] = trim($parts[1]);
9224                  $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9225                  if ($ipend === null) {
9226                      continue;
9227                  }
9228  
9229                  if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9230                      return true;
9231                  }
9232              }
9233  
9234          } else {
9235              // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9236              if (strpos($subnet, ':') !== false) {
9237                  // IPv6.
9238                  if (!$ipv6) {
9239                      continue;
9240                  }
9241                  $parts = explode(':', $subnet);
9242                  $count = count($parts);
9243                  if ($parts[$count-1] === '') {
9244                      unset($parts[$count-1]); // Trim trailing :'s.
9245                      $count--;
9246                      $subnet = implode('.', $parts);
9247                  }
9248                  $isip = cleanremoteaddr($subnet, false); // Normalise.
9249                  if ($isip !== null) {
9250                      if ($isip === $addr) {
9251                          return true;
9252                      }
9253                      continue;
9254                  } else if ($count > 8) {
9255                      continue;
9256                  }
9257                  $zeros = array_fill(0, 8-$count, '0');
9258                  $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9259                  if (address_in_subnet($addr, $subnet)) {
9260                      return true;
9261                  }
9262  
9263              } else {
9264                  // IPv4.
9265                  if ($ipv6) {
9266                      continue;
9267                  }
9268                  $parts = explode('.', $subnet);
9269                  $count = count($parts);
9270                  if ($parts[$count-1] === '') {
9271                      unset($parts[$count-1]); // Trim trailing .
9272                      $count--;
9273                      $subnet = implode('.', $parts);
9274                  }
9275                  if ($count == 4) {
9276                      $subnet = cleanremoteaddr($subnet, false); // Normalise.
9277                      if ($subnet === $addr) {
9278                          return true;
9279                      }
9280                      continue;
9281                  } else if ($count > 4) {
9282                      continue;
9283                  }
9284                  $zeros = array_fill(0, 4-$count, '0');
9285                  $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9286                  if (address_in_subnet($addr, $subnet)) {
9287                      return true;
9288                  }
9289              }
9290          }
9291      }
9292  
9293      return false;
9294  }
9295  
9296  /**
9297   * For outputting debugging info
9298   *
9299   * @param string $string The string to write
9300   * @param string $eol The end of line char(s) to use
9301   * @param string $sleep Period to make the application sleep
9302   *                      This ensures any messages have time to display before redirect
9303   */
9304  function mtrace($string, $eol="\n", $sleep=0) {
9305      global $CFG;
9306  
9307      if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9308          $fn = $CFG->mtrace_wrapper;
9309          $fn($string, $eol);
9310          return;
9311      } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9312          // We must explicitly call the add_line function here.
9313          // Uses of fwrite to STDOUT are not picked up by ob_start.
9314          if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9315              fwrite(STDOUT, $output);
9316          }
9317      } else {
9318          echo $string . $eol;
9319      }
9320  
9321      // Flush again.
9322      flush();
9323  
9324      // Delay to keep message on user's screen in case of subsequent redirect.
9325      if ($sleep) {
9326          sleep($sleep);
9327      }
9328  }
9329  
9330  /**
9331   * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9332   *
9333   * @param Throwable $e the error to ouptput.
9334   */
9335  function mtrace_exception(Throwable $e): void {
9336      $info = get_exception_info($e);
9337  
9338      $message = $info->message;
9339      if ($info->debuginfo) {
9340          $message .= "\n\n" . $info->debuginfo;
9341      }
9342      if ($info->backtrace) {
9343          $message .= "\n\n" . format_backtrace($info->backtrace, true);
9344      }
9345  
9346      mtrace($message);
9347  }
9348  
9349  /**
9350   * Replace 1 or more slashes or backslashes to 1 slash
9351   *
9352   * @param string $path The path to strip
9353   * @return string the path with double slashes removed
9354   */
9355  function cleardoubleslashes ($path) {
9356      return preg_replace('/(\/|\\\){1,}/', '/', $path);
9357  }
9358  
9359  /**
9360   * Is the current ip in a given list?
9361   *
9362   * @param string $list
9363   * @return bool
9364   */
9365  function remoteip_in_list($list) {
9366      $clientip = getremoteaddr(null);
9367  
9368      if (!$clientip) {
9369          // Ensure access on cli.
9370          return true;
9371      }
9372      return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9373  }
9374  
9375  /**
9376   * Returns most reliable client address
9377   *
9378   * @param string $default If an address can't be determined, then return this
9379   * @return string The remote IP address
9380   */
9381  function getremoteaddr($default='0.0.0.0') {
9382      global $CFG;
9383  
9384      if (!isset($CFG->getremoteaddrconf)) {
9385          // This will happen, for example, before just after the upgrade, as the
9386          // user is redirected to the admin screen.
9387          $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9388      } else {
9389          $variablestoskip = $CFG->getremoteaddrconf;
9390      }
9391      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9392          if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9393              $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9394              return $address ? $address : $default;
9395          }
9396      }
9397      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9398          if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9399              $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9400  
9401              $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9402                  global $CFG;
9403                  return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9404              });
9405  
9406              // Multiple proxies can append values to this header including an
9407              // untrusted original request header so we must only trust the last ip.
9408              $address = end($forwardedaddresses);
9409  
9410              if (substr_count($address, ":") > 1) {
9411                  // Remove port and brackets from IPv6.
9412                  if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9413                      $address = $matches[1];
9414                  }
9415              } else {
9416                  // Remove port from IPv4.
9417                  if (substr_count($address, ":") == 1) {
9418                      $parts = explode(":", $address);
9419                      $address = $parts[0];
9420                  }
9421              }
9422  
9423              $address = cleanremoteaddr($address);
9424              return $address ? $address : $default;
9425          }
9426      }
9427      if (!empty($_SERVER['REMOTE_ADDR'])) {
9428          $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9429          return $address ? $address : $default;
9430      } else {
9431          return $default;
9432      }
9433  }
9434  
9435  /**
9436   * Cleans an ip address. Internal addresses are now allowed.
9437   * (Originally local addresses were not allowed.)
9438   *
9439   * @param string $addr IPv4 or IPv6 address
9440   * @param bool $compress use IPv6 address compression
9441   * @return string normalised ip address string, null if error
9442   */
9443  function cleanremoteaddr($addr, $compress=false) {
9444      $addr = trim($addr);
9445  
9446      if (strpos($addr, ':') !== false) {
9447          // Can be only IPv6.
9448          $parts = explode(':', $addr);
9449          $count = count($parts);
9450  
9451          if (strpos($parts[$count-1], '.') !== false) {
9452              // Legacy ipv4 notation.
9453              $last = array_pop($parts);
9454              $ipv4 = cleanremoteaddr($last, true);
9455              if ($ipv4 === null) {
9456                  return null;
9457              }
9458              $bits = explode('.', $ipv4);
9459              $parts[] = dechex($bits[0]).dechex($bits[1]);
9460              $parts[] = dechex($bits[2]).dechex($bits[3]);
9461              $count = count($parts);
9462              $addr = implode(':', $parts);
9463          }
9464  
9465          if ($count < 3 or $count > 8) {
9466              return null; // Severly malformed.
9467          }
9468  
9469          if ($count != 8) {
9470              if (strpos($addr, '::') === false) {
9471                  return null; // Malformed.
9472              }
9473              // Uncompress.
9474              $insertat = array_search('', $parts, true);
9475              $missing = array_fill(0, 1 + 8 - $count, '0');
9476              array_splice($parts, $insertat, 1, $missing);
9477              foreach ($parts as $key => $part) {
9478                  if ($part === '') {
9479                      $parts[$key] = '0';
9480                  }
9481              }
9482          }
9483  
9484          $adr = implode(':', $parts);
9485          if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9486              return null; // Incorrect format - sorry.
9487          }
9488  
9489          // Normalise 0s and case.
9490          $parts = array_map('hexdec', $parts);
9491          $parts = array_map('dechex', $parts);
9492  
9493          $result = implode(':', $parts);
9494  
9495          if (!$compress) {
9496              return $result;
9497          }
9498  
9499          if ($result === '0:0:0:0:0:0:0:0') {
9500              return '::'; // All addresses.
9501          }
9502  
9503          $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9504          if ($compressed !== $result) {
9505              return $compressed;
9506          }
9507  
9508          $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9509          if ($compressed !== $result) {
9510              return $compressed;
9511          }
9512  
9513          $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9514          if ($compressed !== $result) {
9515              return $compressed;
9516          }
9517  
9518          return $result;
9519      }
9520  
9521      // First get all things that look like IPv4 addresses.
9522      $parts = array();
9523      if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9524          return null;
9525      }
9526      unset($parts[0]);
9527  
9528      foreach ($parts as $key => $match) {
9529          if ($match > 255) {
9530              return null;
9531          }
9532          $parts[$key] = (int)$match; // Normalise 0s.
9533      }
9534  
9535      return implode('.', $parts);
9536  }
9537  
9538  
9539  /**
9540   * Is IP address a public address?
9541   *
9542   * @param string $ip The ip to check
9543   * @return bool true if the ip is public
9544   */
9545  function ip_is_public($ip) {
9546      return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9547  }
9548  
9549  /**
9550   * This function will make a complete copy of anything it's given,
9551   * regardless of whether it's an object or not.
9552   *
9553   * @param mixed $thing Something you want cloned
9554   * @return mixed What ever it is you passed it
9555   */
9556  function fullclone($thing) {
9557      return unserialize(serialize($thing));
9558  }
9559  
9560  /**
9561   * Used to make sure that $min <= $value <= $max
9562   *
9563   * Make sure that value is between min, and max
9564   *
9565   * @param int $min The minimum value
9566   * @param int $value The value to check
9567   * @param int $max The maximum value
9568   * @return int
9569   */
9570  function bounded_number($min, $value, $max) {
9571      if ($value < $min) {
9572          return $min;
9573      }
9574      if ($value > $max) {
9575          return $max;
9576      }
9577      return $value;
9578  }
9579  
9580  /**
9581   * Check if there is a nested array within the passed array
9582   *
9583   * @param array $array
9584   * @return bool true if there is a nested array false otherwise
9585   */
9586  function array_is_nested($array) {
9587      foreach ($array as $value) {
9588          if (is_array($value)) {
9589              return true;
9590          }
9591      }
9592      return false;
9593  }
9594  
9595  /**
9596   * get_performance_info() pairs up with init_performance_info()
9597   * loaded in setup.php. Returns an array with 'html' and 'txt'
9598   * values ready for use, and each of the individual stats provided
9599   * separately as well.
9600   *
9601   * @return array
9602   */
9603  function get_performance_info() {
9604      global $CFG, $PERF, $DB, $PAGE;
9605  
9606      $info = array();
9607      $info['txt']  = me() . ' '; // Holds log-friendly representation.
9608  
9609      $info['html'] = '';
9610      if (!empty($CFG->themedesignermode)) {
9611          // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9612          $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9613      }
9614      $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9615  
9616      $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9617  
9618      $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9619      $info['txt'] .= 'time: '.$info['realtime'].'s ';
9620  
9621      // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9622      $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9623  
9624      if (function_exists('memory_get_usage')) {
9625          $info['memory_total'] = memory_get_usage();
9626          $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9627          $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9628          $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9629              $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9630      }
9631  
9632      if (function_exists('memory_get_peak_usage')) {
9633          $info['memory_peak'] = memory_get_peak_usage();
9634          $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9635          $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9636      }
9637  
9638      $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9639      $inc = get_included_files();
9640      $info['includecount'] = count($inc);
9641      $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9642      $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9643  
9644      if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9645          // We can not track more performance before installation or before PAGE init, sorry.
9646          return $info;
9647      }
9648  
9649      $filtermanager = filter_manager::instance();
9650      if (method_exists($filtermanager, 'get_performance_summary')) {
9651          list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9652          $info = array_merge($filterinfo, $info);
9653          foreach ($filterinfo as $key => $value) {
9654              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9655              $info['txt'] .= "$key: $value ";
9656          }
9657      }
9658  
9659      $stringmanager = get_string_manager();
9660      if (method_exists($stringmanager, 'get_performance_summary')) {
9661          list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9662          $info = array_merge($filterinfo, $info);
9663          foreach ($filterinfo as $key => $value) {
9664              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9665              $info['txt'] .= "$key: $value ";
9666          }
9667      }
9668  
9669      if (!empty($PERF->logwrites)) {
9670          $info['logwrites'] = $PERF->logwrites;
9671          $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> ';
9672          $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9673      }
9674  
9675      $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9676      $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9677      $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9678  
9679      if ($DB->want_read_slave()) {
9680          $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9681          $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9682          $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9683      }
9684  
9685      $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9686      $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9687      $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9688  
9689      if (function_exists('posix_times')) {
9690          $ptimes = posix_times();
9691          if (is_array($ptimes)) {
9692              foreach ($ptimes as $key => $val) {
9693                  $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9694              }
9695              $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9696              $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9697              $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9698          }
9699      }
9700  
9701      // Grab the load average for the last minute.
9702      // /proc will only work under some linux configurations
9703      // while uptime is there under MacOSX/Darwin and other unices.
9704      if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9705          list($serverload) = explode(' ', $loadavg[0]);
9706          unset($loadavg);
9707      } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9708          if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9709              $serverload = $matches[1];
9710          } else {
9711              trigger_error('Could not parse uptime output!');
9712          }
9713      }
9714      if (!empty($serverload)) {
9715          $info['serverload'] = $serverload;
9716          $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9717          $info['txt'] .= "serverload: {$info['serverload']} ";
9718      }
9719  
9720      // Display size of session if session started.
9721      if ($si = \core\session\manager::get_performance_info()) {
9722          $info['sessionsize'] = $si['size'];
9723          $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9724          $info['txt'] .= $si['txt'];
9725      }
9726  
9727      // Display time waiting for session if applicable.
9728      if (!empty($PERF->sessionlock['wait'])) {
9729          $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9730          $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9731              'class' => 'sessionwait col-sm-4'
9732          ]);
9733          $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9734      }
9735  
9736      $info['html'] .= '</ul>';
9737      $html = '';
9738      if ($stats = cache_helper::get_stats()) {
9739  
9740          $table = new html_table();
9741          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9742          $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9743          $table->data = [];
9744          $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9745  
9746          $text = 'Caches used (hits/misses/sets): ';
9747          $hits = 0;
9748          $misses = 0;
9749          $sets = 0;
9750          $maxstores = 0;
9751  
9752          // We want to align static caches into their own column.
9753          $hasstatic = false;
9754          foreach ($stats as $definition => $details) {
9755              $numstores = count($details['stores']);
9756              $first = key($details['stores']);
9757              if ($first !== cache_store::STATIC_ACCEL) {
9758                  $numstores++; // Add a blank space for the missing static store.
9759              }
9760              $maxstores = max($maxstores, $numstores);
9761          }
9762  
9763          $storec = 0;
9764  
9765          while ($storec++ < ($maxstores - 2)) {
9766              if ($storec == ($maxstores - 2)) {
9767                  $table->head[] = get_string('mappingfinal', 'cache');
9768              } else {
9769                  $table->head[] = "Store $storec";
9770              }
9771              $table->align[] = 'left';
9772              $table->align[] = 'right';
9773              $table->align[] = 'right';
9774              $table->align[] = 'right';
9775              $table->align[] = 'right';
9776              $table->head[] = 'H';
9777              $table->head[] = 'M';
9778              $table->head[] = 'S';
9779              $table->head[] = 'I/O';
9780          }
9781  
9782          ksort($stats);
9783  
9784          foreach ($stats as $definition => $details) {
9785              switch ($details['mode']) {
9786                  case cache_store::MODE_APPLICATION:
9787                      $modeclass = 'application';
9788                      $mode = ' <span title="application cache">App</span>';
9789                      break;
9790                  case cache_store::MODE_SESSION:
9791                      $modeclass = 'session';
9792                      $mode = ' <span title="session cache">Ses</span>';
9793                      break;
9794                  case cache_store::MODE_REQUEST:
9795                      $modeclass = 'request';
9796                      $mode = ' <span title="request cache">Req</span>';
9797                      break;
9798              }
9799              $row = [$mode, $definition];
9800  
9801              $text .= "$definition {";
9802  
9803              $storec = 0;
9804              foreach ($details['stores'] as $store => $data) {
9805  
9806                  if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9807                      $row[] = '';
9808                      $row[] = '';
9809                      $row[] = '';
9810                      $storec++;
9811                  }
9812  
9813                  $hits   += $data['hits'];
9814                  $misses += $data['misses'];
9815                  $sets   += $data['sets'];
9816                  if ($data['hits'] == 0 and $data['misses'] > 0) {
9817                      $cachestoreclass = 'nohits bg-danger';
9818                  } else if ($data['hits'] < $data['misses']) {
9819                      $cachestoreclass = 'lowhits bg-warning text-dark';
9820                  } else {
9821                      $cachestoreclass = 'hihits';
9822                  }
9823                  $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9824                  $cell = new html_table_cell($store);
9825                  $cell->attributes = ['class' => $cachestoreclass];
9826                  $row[] = $cell;
9827                  $cell = new html_table_cell($data['hits']);
9828                  $cell->attributes = ['class' => $cachestoreclass];
9829                  $row[] = $cell;
9830                  $cell = new html_table_cell($data['misses']);
9831                  $cell->attributes = ['class' => $cachestoreclass];
9832                  $row[] = $cell;
9833  
9834                  if ($store !== cache_store::STATIC_ACCEL) {
9835                      // The static cache is never set.
9836                      $cell = new html_table_cell($data['sets']);
9837                      $cell->attributes = ['class' => $cachestoreclass];
9838                      $row[] = $cell;
9839  
9840                      if ($data['hits'] || $data['sets']) {
9841                          if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9842                              $size = '-';
9843                          } else {
9844                              $size = display_size($data['iobytes'], 1, 'KB');
9845                              if ($data['iobytes'] >= 10 * 1024) {
9846                                  $cachestoreclass = ' bg-warning text-dark';
9847                              }
9848                          }
9849                      } else {
9850                          $size = '';
9851                      }
9852                      $cell = new html_table_cell($size);
9853                      $cell->attributes = ['class' => $cachestoreclass];
9854                      $row[] = $cell;
9855                  }
9856                  $storec++;
9857              }
9858              while ($storec++ < $maxstores) {
9859                  $row[] = '';
9860                  $row[] = '';
9861                  $row[] = '';
9862                  $row[] = '';
9863                  $row[] = '';
9864              }
9865              $text .= '} ';
9866  
9867              $table->data[] = $row;
9868          }
9869  
9870          $html .= html_writer::table($table);
9871  
9872          // Now lets also show sub totals for each cache store.
9873          $storetotals = [];
9874          $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9875          foreach ($stats as $definition => $details) {
9876              foreach ($details['stores'] as $store => $data) {
9877                  if (!array_key_exists($store, $storetotals)) {
9878                      $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9879                  }
9880                  $storetotals[$store]['class']   = $data['class'];
9881                  $storetotals[$store]['hits']   += $data['hits'];
9882                  $storetotals[$store]['misses'] += $data['misses'];
9883                  $storetotals[$store]['sets']   += $data['sets'];
9884                  $storetotal['hits']   += $data['hits'];
9885                  $storetotal['misses'] += $data['misses'];
9886                  $storetotal['sets']   += $data['sets'];
9887                  if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9888                      $storetotals[$store]['iobytes'] += $data['iobytes'];
9889                      $storetotal['iobytes'] += $data['iobytes'];
9890                  }
9891              }
9892          }
9893  
9894          $table = new html_table();
9895          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9896          $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9897          $table->data = [];
9898          $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9899  
9900          ksort($storetotals);
9901  
9902          foreach ($storetotals as $store => $data) {
9903              $row = [];
9904              if ($data['hits'] == 0 and $data['misses'] > 0) {
9905                  $cachestoreclass = 'nohits bg-danger';
9906              } else if ($data['hits'] < $data['misses']) {
9907                  $cachestoreclass = 'lowhits bg-warning text-dark';
9908              } else {
9909                  $cachestoreclass = 'hihits';
9910              }
9911              $cell = new html_table_cell($store);
9912              $cell->attributes = ['class' => $cachestoreclass];
9913              $row[] = $cell;
9914              $cell = new html_table_cell($data['class']);
9915              $cell->attributes = ['class' => $cachestoreclass];
9916              $row[] = $cell;
9917              $cell = new html_table_cell($data['hits']);
9918              $cell->attributes = ['class' => $cachestoreclass];
9919              $row[] = $cell;
9920              $cell = new html_table_cell($data['misses']);
9921              $cell->attributes = ['class' => $cachestoreclass];
9922              $row[] = $cell;
9923              $cell = new html_table_cell($data['sets']);
9924              $cell->attributes = ['class' => $cachestoreclass];
9925              $row[] = $cell;
9926              if ($data['hits'] || $data['sets']) {
9927                  if ($data['iobytes']) {
9928                      $size = display_size($data['iobytes'], 1, 'KB');
9929                  } else {
9930                      $size = '-';
9931                  }
9932              } else {
9933                  $size = '';
9934              }
9935              $cell = new html_table_cell($size);
9936              $cell->attributes = ['class' => $cachestoreclass];
9937              $row[] = $cell;
9938              $table->data[] = $row;
9939          }
9940          if (!empty($storetotal['iobytes'])) {
9941              $size = display_size($storetotal['iobytes'], 1, 'KB');
9942          } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9943              $size = '-';
9944          } else {
9945              $size = '';
9946          }
9947          $row = [
9948              get_string('total'),
9949              '',
9950              $storetotal['hits'],
9951              $storetotal['misses'],
9952              $storetotal['sets'],
9953              $size,
9954          ];
9955          $table->data[] = $row;
9956  
9957          $html .= html_writer::table($table);
9958  
9959          $info['cachesused'] = "$hits / $misses / $sets";
9960          $info['html'] .= $html;
9961          $info['txt'] .= $text.'. ';
9962      } else {
9963          $info['cachesused'] = '0 / 0 / 0';
9964          $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9965          $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9966      }
9967  
9968      // Display lock information if any.
9969      if (!empty($PERF->locks)) {
9970          $table = new html_table();
9971          $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9972          $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9973          $table->align = ['left', 'right', 'center', 'right'];
9974          $table->data = [];
9975          $text = 'Locks (waited/obtained/held):';
9976          foreach ($PERF->locks as $locktiming) {
9977              $row = [];
9978              $row[] = s($locktiming->type . '/' . $locktiming->resource);
9979              $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9980  
9981              // The time we had to wait to get the lock.
9982              $roundedtime = number_format($locktiming->wait, 1);
9983              $cell = new html_table_cell($roundedtime);
9984              if ($locktiming->wait > 0.5) {
9985                  $cell->attributes = ['class' => 'bg-warning text-dark'];
9986              }
9987              $row[] = $cell;
9988              $text .= $roundedtime . '/';
9989  
9990              // Show a tick or cross for success.
9991              $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9992              $text .= ($locktiming->success ? 'y' : 'n') . '/';
9993  
9994              // If applicable, show how long we held the lock before releasing it.
9995              if (property_exists($locktiming, 'held')) {
9996                  $roundedtime = number_format($locktiming->held, 1);
9997                  $cell = new html_table_cell($roundedtime);
9998                  if ($locktiming->held > 0.5) {
9999                      $cell->attributes = ['class' => 'bg-warning text-dark'];
10000                  }
10001                  $row[] = $cell;
10002                  $text .= $roundedtime;
10003              } else {
10004                  $row[] = '-';
10005                  $text .= '-';
10006              }
10007              $text .= ')';
10008  
10009              $table->data[] = $row;
10010          }
10011          $info['html'] .= html_writer::table($table);
10012          $info['txt'] .= $text . '. ';
10013      }
10014  
10015      $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
10016      return $info;
10017  }
10018  
10019  /**
10020   * Renames a file or directory to a unique name within the same directory.
10021   *
10022   * This function is designed to avoid any potential race conditions, and select an unused name.
10023   *
10024   * @param string $filepath Original filepath
10025   * @param string $prefix Prefix to use for the temporary name
10026   * @return string|bool New file path or false if failed
10027   * @since Moodle 3.10
10028   */
10029  function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
10030      $dir = dirname($filepath);
10031      $basename = $dir . '/' . $prefix;
10032      $limit = 0;
10033      while ($limit < 100) {
10034          // Select a new name based on a random number.
10035          $newfilepath = $basename . md5(mt_rand());
10036  
10037          // Attempt a rename to that new name.
10038          if (@rename($filepath, $newfilepath)) {
10039              return $newfilepath;
10040          }
10041  
10042          // The first time, do some sanity checks, maybe it is failing for a good reason and there
10043          // is no point trying 100 times if so.
10044          if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
10045              return false;
10046          }
10047          $limit++;
10048      }
10049      return false;
10050  }
10051  
10052  /**
10053   * Delete directory or only its content
10054   *
10055   * @param string $dir directory path
10056   * @param bool $contentonly
10057   * @return bool success, true also if dir does not exist
10058   */
10059  function remove_dir($dir, $contentonly=false) {
10060      if (!is_dir($dir)) {
10061          // Nothing to do.
10062          return true;
10063      }
10064  
10065      if (!$contentonly) {
10066          // Start by renaming the directory; this will guarantee that other processes don't write to it
10067          // while it is in the process of being deleted.
10068          $tempdir = rename_to_unused_name($dir);
10069          if ($tempdir) {
10070              // If the rename was successful then delete the $tempdir instead.
10071              $dir = $tempdir;
10072          }
10073          // If the rename fails, we will continue through and attempt to delete the directory
10074          // without renaming it since that is likely to at least delete most of the files.
10075      }
10076  
10077      if (!$handle = opendir($dir)) {
10078          return false;
10079      }
10080      $result = true;
10081      while (false!==($item = readdir($handle))) {
10082          if ($item != '.' && $item != '..') {
10083              if (is_dir($dir.'/'.$item)) {
10084                  $result = remove_dir($dir.'/'.$item) && $result;
10085              } else {
10086                  $result = unlink($dir.'/'.$item) && $result;
10087              }
10088          }
10089      }
10090      closedir($handle);
10091      if ($contentonly) {
10092          clearstatcache(); // Make sure file stat cache is properly invalidated.
10093          return $result;
10094      }
10095      $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
10096      clearstatcache(); // Make sure file stat cache is properly invalidated.
10097      return $result;
10098  }
10099  
10100  /**
10101   * Detect if an object or a class contains a given property
10102   * will take an actual object or the name of a class
10103   *
10104   * @param mixed $obj Name of class or real object to test
10105   * @param string $property name of property to find
10106   * @return bool true if property exists
10107   */
10108  function object_property_exists( $obj, $property ) {
10109      if (is_string( $obj )) {
10110          $properties = get_class_vars( $obj );
10111      } else {
10112          $properties = get_object_vars( $obj );
10113      }
10114      return array_key_exists( $property, $properties );
10115  }
10116  
10117  /**
10118   * Converts an object into an associative array
10119   *
10120   * This function converts an object into an associative array by iterating
10121   * over its public properties. Because this function uses the foreach
10122   * construct, Iterators are respected. It works recursively on arrays of objects.
10123   * Arrays and simple values are returned as is.
10124   *
10125   * If class has magic properties, it can implement IteratorAggregate
10126   * and return all available properties in getIterator()
10127   *
10128   * @param mixed $var
10129   * @return array
10130   */
10131  function convert_to_array($var) {
10132      $result = array();
10133  
10134      // Loop over elements/properties.
10135      foreach ($var as $key => $value) {
10136          // Recursively convert objects.
10137          if (is_object($value) || is_array($value)) {
10138              $result[$key] = convert_to_array($value);
10139          } else {
10140              // Simple values are untouched.
10141              $result[$key] = $value;
10142          }
10143      }
10144      return $result;
10145  }
10146  
10147  /**
10148   * Detect a custom script replacement in the data directory that will
10149   * replace an existing moodle script
10150   *
10151   * @return string|bool full path name if a custom script exists, false if no custom script exists
10152   */
10153  function custom_script_path() {
10154      global $CFG, $SCRIPT;
10155  
10156      if ($SCRIPT === null) {
10157          // Probably some weird external script.
10158          return false;
10159      }
10160  
10161      $scriptpath = $CFG->customscripts . $SCRIPT;
10162  
10163      // Check the custom script exists.
10164      if (file_exists($scriptpath) and is_file($scriptpath)) {
10165          return $scriptpath;
10166      } else {
10167          return false;
10168      }
10169  }
10170  
10171  /**
10172   * Returns whether or not the user object is a remote MNET user. This function
10173   * is in moodlelib because it does not rely on loading any of the MNET code.
10174   *
10175   * @param object $user A valid user object
10176   * @return bool        True if the user is from a remote Moodle.
10177   */
10178  function is_mnet_remote_user($user) {
10179      global $CFG;
10180  
10181      if (!isset($CFG->mnet_localhost_id)) {
10182          include_once($CFG->dirroot . '/mnet/lib.php');
10183          $env = new mnet_environment();
10184          $env->init();
10185          unset($env);
10186      }
10187  
10188      return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10189  }
10190  
10191  /**
10192   * This function will search for browser prefereed languages, setting Moodle
10193   * to use the best one available if $SESSION->lang is undefined
10194   */
10195  function setup_lang_from_browser() {
10196      global $CFG, $SESSION, $USER;
10197  
10198      if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10199          // Lang is defined in session or user profile, nothing to do.
10200          return;
10201      }
10202  
10203      if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10204          return;
10205      }
10206  
10207      // Extract and clean langs from headers.
10208      $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10209      $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10210      $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10211      $langs = array();
10212  
10213      $order = 1.0;
10214      foreach ($rawlangs as $lang) {
10215          if (strpos($lang, ';') === false) {
10216              $langs[(string)$order] = $lang;
10217              $order = $order-0.01;
10218          } else {
10219              $parts = explode(';', $lang);
10220              $pos = strpos($parts[1], '=');
10221              $langs[substr($parts[1], $pos+1)] = $parts[0];
10222          }
10223      }
10224      krsort($langs, SORT_NUMERIC);
10225  
10226      // Look for such langs under standard locations.
10227      foreach ($langs as $lang) {
10228          // Clean it properly for include.
10229          $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10230          if (get_string_manager()->translation_exists($lang, false)) {
10231              // If the translation for this language exists then try to set it
10232              // for the rest of the session, if this is a read only session then
10233              // we can only set it temporarily in $CFG.
10234              if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
10235                  $CFG->lang = $lang;
10236              } else {
10237                  $SESSION->lang = $lang;
10238              }
10239              // We have finished. Go out.
10240              break;
10241          }
10242      }
10243      return;
10244  }
10245  
10246  /**
10247   * Check if $url matches anything in proxybypass list
10248   *
10249   * Any errors just result in the proxy being used (least bad)
10250   *
10251   * @param string $url url to check
10252   * @return boolean true if we should bypass the proxy
10253   */
10254  function is_proxybypass( $url ) {
10255      global $CFG;
10256  
10257      // Sanity check.
10258      if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10259          return false;
10260      }
10261  
10262      // Get the host part out of the url.
10263      if (!$host = parse_url( $url, PHP_URL_HOST )) {
10264          return false;
10265      }
10266  
10267      // Get the possible bypass hosts into an array.
10268      $matches = explode( ',', $CFG->proxybypass );
10269  
10270      // Check for a exact match on the IP or in the domains.
10271      $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10272      $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10273  
10274      if ($isdomaininallowedlist || $isipinsubnetlist) {
10275          return true;
10276      }
10277  
10278      // Nothing matched.
10279      return false;
10280  }
10281  
10282  /**
10283   * Check if the passed navigation is of the new style
10284   *
10285   * @param mixed $navigation
10286   * @return bool true for yes false for no
10287   */
10288  function is_newnav($navigation) {
10289      if (is_array($navigation) && !empty($navigation['newnav'])) {
10290          return true;
10291      } else {
10292          return false;
10293      }
10294  }
10295  
10296  /**
10297   * Checks whether the given variable name is defined as a variable within the given object.
10298   *
10299   * This will NOT work with stdClass objects, which have no class variables.
10300   *
10301   * @param string $var The variable name
10302   * @param object $object The object to check
10303   * @return boolean
10304   */
10305  function in_object_vars($var, $object) {
10306      $classvars = get_class_vars(get_class($object));
10307      $classvars = array_keys($classvars);
10308      return in_array($var, $classvars);
10309  }
10310  
10311  /**
10312   * Returns an array without repeated objects.
10313   * This function is similar to array_unique, but for arrays that have objects as values
10314   *
10315   * @param array $array
10316   * @param bool $keepkeyassoc
10317   * @return array
10318   */
10319  function object_array_unique($array, $keepkeyassoc = true) {
10320      $duplicatekeys = array();
10321      $tmp         = array();
10322  
10323      foreach ($array as $key => $val) {
10324          // Convert objects to arrays, in_array() does not support objects.
10325          if (is_object($val)) {
10326              $val = (array)$val;
10327          }
10328  
10329          if (!in_array($val, $tmp)) {
10330              $tmp[] = $val;
10331          } else {
10332              $duplicatekeys[] = $key;
10333          }
10334      }
10335  
10336      foreach ($duplicatekeys as $key) {
10337          unset($array[$key]);
10338      }
10339  
10340      return $keepkeyassoc ? $array : array_values($array);
10341  }
10342  
10343  /**
10344   * Is a userid the primary administrator?
10345   *
10346   * @param int $userid int id of user to check
10347   * @return boolean
10348   */
10349  function is_primary_admin($userid) {
10350      $primaryadmin =  get_admin();
10351  
10352      if ($userid == $primaryadmin->id) {
10353          return true;
10354      } else {
10355          return false;
10356      }
10357  }
10358  
10359  /**
10360   * Returns the site identifier
10361   *
10362   * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10363   */
10364  function get_site_identifier() {
10365      global $CFG;
10366      // Check to see if it is missing. If so, initialise it.
10367      if (empty($CFG->siteidentifier)) {
10368          set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10369      }
10370      // Return it.
10371      return $CFG->siteidentifier;
10372  }
10373  
10374  /**
10375   * Check whether the given password has no more than the specified
10376   * number of consecutive identical characters.
10377   *
10378   * @param string $password   password to be checked against the password policy
10379   * @param integer $maxchars  maximum number of consecutive identical characters
10380   * @return bool
10381   */
10382  function check_consecutive_identical_characters($password, $maxchars) {
10383  
10384      if ($maxchars < 1) {
10385          return true; // Zero 0 is to disable this check.
10386      }
10387      if (strlen($password) <= $maxchars) {
10388          return true; // Too short to fail this test.
10389      }
10390  
10391      $previouschar = '';
10392      $consecutivecount = 1;
10393      foreach (str_split($password) as $char) {
10394          if ($char != $previouschar) {
10395              $consecutivecount = 1;
10396          } else {
10397              $consecutivecount++;
10398              if ($consecutivecount > $maxchars) {
10399                  return false; // Check failed already.
10400              }
10401          }
10402  
10403          $previouschar = $char;
10404      }
10405  
10406      return true;
10407  }
10408  
10409  /**
10410   * Helper function to do partial function binding.
10411   * so we can use it for preg_replace_callback, for example
10412   * this works with php functions, user functions, static methods and class methods
10413   * it returns you a callback that you can pass on like so:
10414   *
10415   * $callback = partial('somefunction', $arg1, $arg2);
10416   *     or
10417   * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10418   *     or even
10419   * $obj = new someclass();
10420   * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10421   *
10422   * and then the arguments that are passed through at calltime are appended to the argument list.
10423   *
10424   * @param mixed $function a php callback
10425   * @param mixed $arg1,... $argv arguments to partially bind with
10426   * @return array Array callback
10427   */
10428  function partial() {
10429      if (!class_exists('partial')) {
10430          /**
10431           * Used to manage function binding.
10432           * @copyright  2009 Penny Leach
10433           * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10434           */
10435          class partial{
10436              /** @var array */
10437              public $values = array();
10438              /** @var string The function to call as a callback. */
10439              public $func;
10440              /**
10441               * Constructor
10442               * @param string $func
10443               * @param array $args
10444               */
10445              public function __construct($func, $args) {
10446                  $this->values = $args;
10447                  $this->func = $func;
10448              }
10449              /**
10450               * Calls the callback function.
10451               * @return mixed
10452               */
10453              public function method() {
10454                  $args = func_get_args();
10455                  return call_user_func_array($this->func, array_merge($this->values, $args));
10456              }
10457          }
10458      }
10459      $args = func_get_args();
10460      $func = array_shift($args);
10461      $p = new partial($func, $args);
10462      return array($p, 'method');
10463  }
10464  
10465  /**
10466   * helper function to load up and initialise the mnet environment
10467   * this must be called before you use mnet functions.
10468   *
10469   * @return mnet_environment the equivalent of old $MNET global
10470   */
10471  function get_mnet_environment() {
10472      global $CFG;
10473      require_once($CFG->dirroot . '/mnet/lib.php');
10474      static $instance = null;
10475      if (empty($instance)) {
10476          $instance = new mnet_environment();
10477          $instance->init();
10478      }
10479      return $instance;
10480  }
10481  
10482  /**
10483   * during xmlrpc server code execution, any code wishing to access
10484   * information about the remote peer must use this to get it.
10485   *
10486   * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10487   */
10488  function get_mnet_remote_client() {
10489      if (!defined('MNET_SERVER')) {
10490          debugging(get_string('notinxmlrpcserver', 'mnet'));
10491          return false;
10492      }
10493      global $MNET_REMOTE_CLIENT;
10494      if (isset($MNET_REMOTE_CLIENT)) {
10495          return $MNET_REMOTE_CLIENT;
10496      }
10497      return false;
10498  }
10499  
10500  /**
10501   * during the xmlrpc server code execution, this will be called
10502   * to setup the object returned by {@link get_mnet_remote_client}
10503   *
10504   * @param mnet_remote_client $client the client to set up
10505   * @throws moodle_exception
10506   */
10507  function set_mnet_remote_client($client) {
10508      if (!defined('MNET_SERVER')) {
10509          throw new moodle_exception('notinxmlrpcserver', 'mnet');
10510      }
10511      global $MNET_REMOTE_CLIENT;
10512      $MNET_REMOTE_CLIENT = $client;
10513  }
10514  
10515  /**
10516   * return the jump url for a given remote user
10517   * this is used for rewriting forum post links in emails, etc
10518   *
10519   * @param stdclass $user the user to get the idp url for
10520   */
10521  function mnet_get_idp_jump_url($user) {
10522      global $CFG;
10523  
10524      static $mnetjumps = array();
10525      if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10526          $idp = mnet_get_peer_host($user->mnethostid);
10527          $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10528          $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10529      }
10530      return $mnetjumps[$user->mnethostid];
10531  }
10532  
10533  /**
10534   * Gets the homepage to use for the current user
10535   *
10536   * @return int One of HOMEPAGE_*
10537   */
10538  function get_home_page() {
10539      global $CFG;
10540  
10541      if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10542          // If dashboard is disabled, home will be set to default page.
10543          $defaultpage = get_default_home_page();
10544          if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10545              if (!empty($CFG->enabledashboard)) {
10546                  return HOMEPAGE_MY;
10547              } else {
10548                  return $defaultpage;
10549              }
10550          } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10551              return HOMEPAGE_MYCOURSES;
10552          } else {
10553              $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10554              if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10555                  // If the user was using the dashboard but it's disabled, return the default home page.
10556                  $userhomepage = $defaultpage;
10557              }
10558              return $userhomepage;
10559          }
10560      }
10561      return HOMEPAGE_SITE;
10562  }
10563  
10564  /**
10565   * Returns the default home page to display if current one is not defined or can't be applied.
10566   * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10567   *
10568   * @return int The default home page.
10569   */
10570  function get_default_home_page(): int {
10571      global $CFG;
10572  
10573      return !empty($CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10574  }
10575  
10576  /**
10577   * Gets the name of a course to be displayed when showing a list of courses.
10578   * By default this is just $course->fullname but user can configure it. The
10579   * result of this function should be passed through print_string.
10580   * @param stdClass|core_course_list_element $course Moodle course object
10581   * @return string Display name of course (either fullname or short + fullname)
10582   */
10583  function get_course_display_name_for_list($course) {
10584      global $CFG;
10585      if (!empty($CFG->courselistshortnames)) {
10586          if (!($course instanceof stdClass)) {
10587              $course = (object)convert_to_array($course);
10588          }
10589          return get_string('courseextendednamedisplay', '', $course);
10590      } else {
10591          return $course->fullname;
10592      }
10593  }
10594  
10595  /**
10596   * Safe analogue of unserialize() that can only parse arrays
10597   *
10598   * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10599   *
10600   * @param string $expression
10601   * @return array|bool either parsed array or false if parsing was impossible.
10602   */
10603  function unserialize_array($expression) {
10604  
10605      // Check the expression is an array.
10606      if (!preg_match('/^a:(\d+):/', $expression)) {
10607          return false;
10608      }
10609  
10610      $values = (array) unserialize_object($expression);
10611  
10612      // Callback that returns true if the given value is an unserialized object, executes recursively.
10613      $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10614          if (is_array($value)) {
10615              return (bool) array_filter($value, $invalidvaluecallback);
10616          }
10617          return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10618      };
10619  
10620      // Iterate over the result to ensure there are no stray objects.
10621      if (array_filter($values, $invalidvaluecallback)) {
10622          return false;
10623      }
10624  
10625      return $values;
10626  }
10627  
10628  /**
10629   * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10630   *
10631   * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10632   * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10633   * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10634   *
10635   * @param string $input
10636   * @return stdClass
10637   */
10638  function unserialize_object(string $input): stdClass {
10639      $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10640      return (object) $instance;
10641  }
10642  
10643  /**
10644   * The lang_string class
10645   *
10646   * This special class is used to create an object representation of a string request.
10647   * It is special because processing doesn't occur until the object is first used.
10648   * The class was created especially to aid performance in areas where strings were
10649   * required to be generated but were not necessarily used.
10650   * As an example the admin tree when generated uses over 1500 strings, of which
10651   * normally only 1/3 are ever actually printed at any time.
10652   * The performance advantage is achieved by not actually processing strings that
10653   * arn't being used, as such reducing the processing required for the page.
10654   *
10655   * How to use the lang_string class?
10656   *     There are two methods of using the lang_string class, first through the
10657   *     forth argument of the get_string function, and secondly directly.
10658   *     The following are examples of both.
10659   * 1. Through get_string calls e.g.
10660   *     $string = get_string($identifier, $component, $a, true);
10661   *     $string = get_string('yes', 'moodle', null, true);
10662   * 2. Direct instantiation
10663   *     $string = new lang_string($identifier, $component, $a, $lang);
10664   *     $string = new lang_string('yes');
10665   *
10666   * How do I use a lang_string object?
10667   *     The lang_string object makes use of a magic __toString method so that you
10668   *     are able to use the object exactly as you would use a string in most cases.
10669   *     This means you are able to collect it into a variable and then directly
10670   *     echo it, or concatenate it into another string, or similar.
10671   *     The other thing you can do is manually get the string by calling the
10672   *     lang_strings out method e.g.
10673   *         $string = new lang_string('yes');
10674   *         $string->out();
10675   *     Also worth noting is that the out method can take one argument, $lang which
10676   *     allows the developer to change the language on the fly.
10677   *
10678   * When should I use a lang_string object?
10679   *     The lang_string object is designed to be used in any situation where a
10680   *     string may not be needed, but needs to be generated.
10681   *     The admin tree is a good example of where lang_string objects should be
10682   *     used.
10683   *     A more practical example would be any class that requries strings that may
10684   *     not be printed (after all classes get renderer by renderers and who knows
10685   *     what they will do ;))
10686   *
10687   * When should I not use a lang_string object?
10688   *     Don't use lang_strings when you are going to use a string immediately.
10689   *     There is no need as it will be processed immediately and there will be no
10690   *     advantage, and in fact perhaps a negative hit as a class has to be
10691   *     instantiated for a lang_string object, however get_string won't require
10692   *     that.
10693   *
10694   * Limitations:
10695   * 1. You cannot use a lang_string object as an array offset. Doing so will
10696   *     result in PHP throwing an error. (You can use it as an object property!)
10697   *
10698   * @package    core
10699   * @category   string
10700   * @copyright  2011 Sam Hemelryk
10701   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10702   */
10703  class lang_string {
10704  
10705      /** @var string The strings identifier */
10706      protected $identifier;
10707      /** @var string The strings component. Default '' */
10708      protected $component = '';
10709      /** @var array|stdClass Any arguments required for the string. Default null */
10710      protected $a = null;
10711      /** @var string The language to use when processing the string. Default null */
10712      protected $lang = null;
10713  
10714      /** @var string The processed string (once processed) */
10715      protected $string = null;
10716  
10717      /**
10718       * A special boolean. If set to true then the object has been woken up and
10719       * cannot be regenerated. If this is set then $this->string MUST be used.
10720       * @var bool
10721       */
10722      protected $forcedstring = false;
10723  
10724      /**
10725       * Constructs a lang_string object
10726       *
10727       * This function should do as little processing as possible to ensure the best
10728       * performance for strings that won't be used.
10729       *
10730       * @param string $identifier The strings identifier
10731       * @param string $component The strings component
10732       * @param stdClass|array|mixed $a Any arguments the string requires
10733       * @param string $lang The language to use when processing the string.
10734       * @throws coding_exception
10735       */
10736      public function __construct($identifier, $component = '', $a = null, $lang = null) {
10737          if (empty($component)) {
10738              $component = 'moodle';
10739          }
10740  
10741          $this->identifier = $identifier;
10742          $this->component = $component;
10743          $this->lang = $lang;
10744  
10745          // We MUST duplicate $a to ensure that it if it changes by reference those
10746          // changes are not carried across.
10747          // To do this we always ensure $a or its properties/values are strings
10748          // and that any properties/values that arn't convertable are forgotten.
10749          if ($a !== null) {
10750              if (is_scalar($a)) {
10751                  $this->a = $a;
10752              } else if ($a instanceof lang_string) {
10753                  $this->a = $a->out();
10754              } else if (is_object($a) or is_array($a)) {
10755                  $a = (array)$a;
10756                  $this->a = array();
10757                  foreach ($a as $key => $value) {
10758                      // Make sure conversion errors don't get displayed (results in '').
10759                      if (is_array($value)) {
10760                          $this->a[$key] = '';
10761                      } else if (is_object($value)) {
10762                          if (method_exists($value, '__toString')) {
10763                              $this->a[$key] = $value->__toString();
10764                          } else {
10765                              $this->a[$key] = '';
10766                          }
10767                      } else {
10768                          $this->a[$key] = (string)$value;
10769                      }
10770                  }
10771              }
10772          }
10773  
10774          if (debugging(false, DEBUG_DEVELOPER)) {
10775              if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10776                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10777              }
10778              if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10779                  throw new coding_exception('Invalid string compontent. Please check your string definition');
10780              }
10781              if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10782                  debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10783              }
10784          }
10785      }
10786  
10787      /**
10788       * Processes the string.
10789       *
10790       * This function actually processes the string, stores it in the string property
10791       * and then returns it.
10792       * You will notice that this function is VERY similar to the get_string method.
10793       * That is because it is pretty much doing the same thing.
10794       * However as this function is an upgrade it isn't as tolerant to backwards
10795       * compatibility.
10796       *
10797       * @return string
10798       * @throws coding_exception
10799       */
10800      protected function get_string() {
10801          global $CFG;
10802  
10803          // Check if we need to process the string.
10804          if ($this->string === null) {
10805              // Check the quality of the identifier.
10806              if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10807                  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);
10808              }
10809  
10810              // Process the string.
10811              $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10812              // Debugging feature lets you display string identifier and component.
10813              if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10814                  $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10815              }
10816          }
10817          // Return the string.
10818          return $this->string;
10819      }
10820  
10821      /**
10822       * Returns the string
10823       *
10824       * @param string $lang The langauge to use when processing the string
10825       * @return string
10826       */
10827      public function out($lang = null) {
10828          if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10829              if ($this->forcedstring) {
10830                  debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10831                  return $this->get_string();
10832              }
10833              $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10834              return $translatedstring->out();
10835          }
10836          return $this->get_string();
10837      }
10838  
10839      /**
10840       * Magic __toString method for printing a string
10841       *
10842       * @return string
10843       */
10844      public function __toString() {
10845          return $this->get_string();
10846      }
10847  
10848      /**
10849       * Magic __set_state method used for var_export
10850       *
10851       * @param array $array
10852       * @return self
10853       */
10854      public static function __set_state(array $array): self {
10855          $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10856          $tmp->string = $array['string'];
10857          $tmp->forcedstring = $array['forcedstring'];
10858          return $tmp;
10859      }
10860  
10861      /**
10862       * Prepares the lang_string for sleep and stores only the forcedstring and
10863       * string properties... the string cannot be regenerated so we need to ensure
10864       * it is generated for this.
10865       *
10866       * @return string
10867       */
10868      public function __sleep() {
10869          $this->get_string();
10870          $this->forcedstring = true;
10871          return array('forcedstring', 'string', 'lang');
10872      }
10873  
10874      /**
10875       * Returns the identifier.
10876       *
10877       * @return string
10878       */
10879      public function get_identifier() {
10880          return $this->identifier;
10881      }
10882  
10883      /**
10884       * Returns the component.
10885       *
10886       * @return string
10887       */
10888      public function get_component() {
10889          return $this->component;
10890      }
10891  }
10892  
10893  /**
10894   * Get human readable name describing the given callable.
10895   *
10896   * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10897   * It does not check if the callable actually exists.
10898   *
10899   * @param callable|string|array $callable
10900   * @return string|bool Human readable name of callable, or false if not a valid callable.
10901   */
10902  function get_callable_name($callable) {
10903  
10904      if (!is_callable($callable, true, $name)) {
10905          return false;
10906  
10907      } else {
10908          return $name;
10909      }
10910  }
10911  
10912  /**
10913   * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10914   * Never put your faith on this function and rely on its accuracy as there might be false positives.
10915   * It just performs some simple checks, and mainly is used for places where we want to hide some options
10916   * such as site registration when $CFG->wwwroot is not publicly accessible.
10917   * Good thing is there is no false negative.
10918   * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10919   *
10920   * @return bool
10921   */
10922  function site_is_public() {
10923      global $CFG;
10924  
10925      // Return early if site admin has forced this setting.
10926      if (isset($CFG->site_is_public)) {
10927          return (bool)$CFG->site_is_public;
10928      }
10929  
10930      $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10931  
10932      if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10933          $ispublic = false;
10934      } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10935          $ispublic = false;
10936      } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10937          $ispublic = false;
10938      } else {
10939          $ispublic = true;
10940      }
10941  
10942      return $ispublic;
10943  }