Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [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  /**
 401   * Required password pepper entropy.
 402   */
 403  define ('PEPPER_ENTROPY', 112);
 404  
 405  // Feature constants.
 406  // Used for plugin_supports() to report features that are, or are not, supported by a module.
 407  
 408  /** True if module can provide a grade */
 409  define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
 410  /** True if module supports outcomes */
 411  define('FEATURE_GRADE_OUTCOMES', 'outcomes');
 412  /** True if module supports advanced grading methods */
 413  define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
 414  /** True if module controls the grade visibility over the gradebook */
 415  define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
 416  /** True if module supports plagiarism plugins */
 417  define('FEATURE_PLAGIARISM', 'plagiarism');
 418  
 419  /** True if module has code to track whether somebody viewed it */
 420  define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
 421  /** True if module has custom completion rules */
 422  define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
 423  
 424  /** True if module has no 'view' page (like label) */
 425  define('FEATURE_NO_VIEW_LINK', 'viewlink');
 426  /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
 427  define('FEATURE_IDNUMBER', 'idnumber');
 428  /** True if module supports groups */
 429  define('FEATURE_GROUPS', 'groups');
 430  /** True if module supports groupings */
 431  define('FEATURE_GROUPINGS', 'groupings');
 432  /**
 433   * True if module supports groupmembersonly (which no longer exists)
 434   * @deprecated Since Moodle 2.8
 435   */
 436  define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
 437  
 438  /** Type of module */
 439  define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
 440  /** True if module supports intro editor */
 441  define('FEATURE_MOD_INTRO', 'mod_intro');
 442  /** True if module has default completion */
 443  define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
 444  
 445  define('FEATURE_COMMENT', 'comment');
 446  
 447  define('FEATURE_RATE', 'rate');
 448  /** True if module supports backup/restore of moodle2 format */
 449  define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
 450  
 451  /** True if module can show description on course main page */
 452  define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
 453  
 454  /** True if module uses the question bank */
 455  define('FEATURE_USES_QUESTIONS', 'usesquestions');
 456  
 457  /**
 458   * Maximum filename char size
 459   */
 460  define('MAX_FILENAME_SIZE', 100);
 461  
 462  /** Unspecified module archetype */
 463  define('MOD_ARCHETYPE_OTHER', 0);
 464  /** Resource-like type module */
 465  define('MOD_ARCHETYPE_RESOURCE', 1);
 466  /** Assignment module archetype */
 467  define('MOD_ARCHETYPE_ASSIGNMENT', 2);
 468  /** System (not user-addable) module archetype */
 469  define('MOD_ARCHETYPE_SYSTEM', 3);
 470  
 471  /** Type of module */
 472  define('FEATURE_MOD_PURPOSE', 'mod_purpose');
 473  /** Module purpose administration */
 474  define('MOD_PURPOSE_ADMINISTRATION', 'administration');
 475  /** Module purpose assessment */
 476  define('MOD_PURPOSE_ASSESSMENT', 'assessment');
 477  /** Module purpose communication */
 478  define('MOD_PURPOSE_COLLABORATION', 'collaboration');
 479  /** Module purpose communication */
 480  define('MOD_PURPOSE_COMMUNICATION', 'communication');
 481  /** Module purpose content */
 482  define('MOD_PURPOSE_CONTENT', 'content');
 483  /** Module purpose interface */
 484  define('MOD_PURPOSE_INTERFACE', 'interface');
 485  /** Module purpose other */
 486  define('MOD_PURPOSE_OTHER', 'other');
 487  
 488  /**
 489   * Security token used for allowing access
 490   * from external application such as web services.
 491   * Scripts do not use any session, performance is relatively
 492   * low because we need to load access info in each request.
 493   * Scripts are executed in parallel.
 494   */
 495  define('EXTERNAL_TOKEN_PERMANENT', 0);
 496  
 497  /**
 498   * Security token used for allowing access
 499   * of embedded applications, the code is executed in the
 500   * active user session. Token is invalidated after user logs out.
 501   * Scripts are executed serially - normal session locking is used.
 502   */
 503  define('EXTERNAL_TOKEN_EMBEDDED', 1);
 504  
 505  /**
 506   * The home page should be the site home
 507   */
 508  define('HOMEPAGE_SITE', 0);
 509  /**
 510   * The home page should be the users my page
 511   */
 512  define('HOMEPAGE_MY', 1);
 513  /**
 514   * The home page can be chosen by the user
 515   */
 516  define('HOMEPAGE_USER', 2);
 517  /**
 518   * The home page should be the users my courses page
 519   */
 520  define('HOMEPAGE_MYCOURSES', 3);
 521  
 522  /**
 523   * URL of the Moodle sites registration portal.
 524   */
 525  defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
 526  
 527  /**
 528   * URL of the statistic server public key.
 529   */
 530  defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
 531  
 532  /**
 533   * Moodle mobile app service name
 534   */
 535  define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
 536  
 537  /**
 538   * Indicates the user has the capabilities required to ignore activity and course file size restrictions
 539   */
 540  define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 541  
 542  /**
 543   * Course display settings: display all sections on one page.
 544   */
 545  define('COURSE_DISPLAY_SINGLEPAGE', 0);
 546  /**
 547   * Course display settings: split pages into a page per section.
 548   */
 549  define('COURSE_DISPLAY_MULTIPAGE', 1);
 550  
 551  /**
 552   * Authentication constant: String used in password field when password is not stored.
 553   */
 554  define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
 555  
 556  /**
 557   * Email from header to never include via information.
 558   */
 559  define('EMAIL_VIA_NEVER', 0);
 560  
 561  /**
 562   * Email from header to always include via information.
 563   */
 564  define('EMAIL_VIA_ALWAYS', 1);
 565  
 566  /**
 567   * Email from header to only include via information if the address is no-reply.
 568   */
 569  define('EMAIL_VIA_NO_REPLY_ONLY', 2);
 570  
 571  /**
 572   * Contact site support form/link disabled.
 573   */
 574  define('CONTACT_SUPPORT_DISABLED', 0);
 575  
 576  /**
 577   * Contact site support form/link only available to authenticated users.
 578   */
 579  define('CONTACT_SUPPORT_AUTHENTICATED', 1);
 580  
 581  /**
 582   * Contact site support form/link available to anyone visiting the site.
 583   */
 584  define('CONTACT_SUPPORT_ANYONE', 2);
 585  
 586  /**
 587   * Maximum number of characters for password.
 588   */
 589  define('MAX_PASSWORD_CHARACTERS', 128);
 590  
 591  // PARAMETER HANDLING.
 592  
 593  /**
 594   * Returns a particular value for the named variable, taken from
 595   * POST or GET.  If the parameter doesn't exist then an error is
 596   * thrown because we require this variable.
 597   *
 598   * This function should be used to initialise all required values
 599   * in a script that are based on parameters.  Usually it will be
 600   * used like this:
 601   *    $id = required_param('id', PARAM_INT);
 602   *
 603   * Please note the $type parameter is now required and the value can not be array.
 604   *
 605   * @param string $parname the name of the page parameter we want
 606   * @param string $type expected type of parameter
 607   * @return mixed
 608   * @throws coding_exception
 609   */
 610  function required_param($parname, $type) {
 611      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 612          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
 613      }
 614      // POST has precedence.
 615      if (isset($_POST[$parname])) {
 616          $param = $_POST[$parname];
 617      } else if (isset($_GET[$parname])) {
 618          $param = $_GET[$parname];
 619      } else {
 620          throw new \moodle_exception('missingparam', '', '', $parname);
 621      }
 622  
 623      if (is_array($param)) {
 624          debugging('Invalid array parameter detected in required_param(): '.$parname);
 625          // TODO: switch to fatal error in Moodle 2.3.
 626          return required_param_array($parname, $type);
 627      }
 628  
 629      return clean_param($param, $type);
 630  }
 631  
 632  /**
 633   * Returns a particular array value for the named variable, taken from
 634   * POST or GET.  If the parameter doesn't exist then an error is
 635   * thrown because we require this variable.
 636   *
 637   * This function should be used to initialise all required values
 638   * in a script that are based on parameters.  Usually it will be
 639   * used like this:
 640   *    $ids = required_param_array('ids', PARAM_INT);
 641   *
 642   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 643   *
 644   * @param string $parname the name of the page parameter we want
 645   * @param string $type expected type of parameter
 646   * @return array
 647   * @throws coding_exception
 648   */
 649  function required_param_array($parname, $type) {
 650      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 651          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
 652      }
 653      // POST has precedence.
 654      if (isset($_POST[$parname])) {
 655          $param = $_POST[$parname];
 656      } else if (isset($_GET[$parname])) {
 657          $param = $_GET[$parname];
 658      } else {
 659          throw new \moodle_exception('missingparam', '', '', $parname);
 660      }
 661      if (!is_array($param)) {
 662          throw new \moodle_exception('missingparam', '', '', $parname);
 663      }
 664  
 665      $result = array();
 666      foreach ($param as $key => $value) {
 667          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 668              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
 669              continue;
 670          }
 671          $result[$key] = clean_param($value, $type);
 672      }
 673  
 674      return $result;
 675  }
 676  
 677  /**
 678   * Returns a particular value for the named variable, taken from
 679   * POST or GET, otherwise returning a given default.
 680   *
 681   * This function should be used to initialise all optional values
 682   * in a script that are based on parameters.  Usually it will be
 683   * used like this:
 684   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
 685   *
 686   * Please note the $type parameter is now required and the value can not be array.
 687   *
 688   * @param string $parname the name of the page parameter we want
 689   * @param mixed  $default the default value to return if nothing is found
 690   * @param string $type expected type of parameter
 691   * @return mixed
 692   * @throws coding_exception
 693   */
 694  function optional_param($parname, $default, $type) {
 695      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 696          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 697      }
 698  
 699      // POST has precedence.
 700      if (isset($_POST[$parname])) {
 701          $param = $_POST[$parname];
 702      } else if (isset($_GET[$parname])) {
 703          $param = $_GET[$parname];
 704      } else {
 705          return $default;
 706      }
 707  
 708      if (is_array($param)) {
 709          debugging('Invalid array parameter detected in required_param(): '.$parname);
 710          // TODO: switch to $default in Moodle 2.3.
 711          return optional_param_array($parname, $default, $type);
 712      }
 713  
 714      return clean_param($param, $type);
 715  }
 716  
 717  /**
 718   * Returns a particular array value for the named variable, taken from
 719   * POST or GET, otherwise returning a given default.
 720   *
 721   * This function should be used to initialise all optional values
 722   * in a script that are based on parameters.  Usually it will be
 723   * used like this:
 724   *    $ids = optional_param('id', array(), PARAM_INT);
 725   *
 726   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 727   *
 728   * @param string $parname the name of the page parameter we want
 729   * @param mixed $default the default value to return if nothing is found
 730   * @param string $type expected type of parameter
 731   * @return array
 732   * @throws coding_exception
 733   */
 734  function optional_param_array($parname, $default, $type) {
 735      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 736          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 737      }
 738  
 739      // POST has precedence.
 740      if (isset($_POST[$parname])) {
 741          $param = $_POST[$parname];
 742      } else if (isset($_GET[$parname])) {
 743          $param = $_GET[$parname];
 744      } else {
 745          return $default;
 746      }
 747      if (!is_array($param)) {
 748          debugging('optional_param_array() expects array parameters only: '.$parname);
 749          return $default;
 750      }
 751  
 752      $result = array();
 753      foreach ($param as $key => $value) {
 754          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 755              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
 756              continue;
 757          }
 758          $result[$key] = clean_param($value, $type);
 759      }
 760  
 761      return $result;
 762  }
 763  
 764  /**
 765   * Strict validation of parameter values, the values are only converted
 766   * to requested PHP type. Internally it is using clean_param, the values
 767   * before and after cleaning must be equal - otherwise
 768   * an invalid_parameter_exception is thrown.
 769   * Objects and classes are not accepted.
 770   *
 771   * @param mixed $param
 772   * @param string $type PARAM_ constant
 773   * @param bool $allownull are nulls valid value?
 774   * @param string $debuginfo optional debug information
 775   * @return mixed the $param value converted to PHP type
 776   * @throws invalid_parameter_exception if $param is not of given type
 777   */
 778  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
 779      if (is_null($param)) {
 780          if ($allownull == NULL_ALLOWED) {
 781              return null;
 782          } else {
 783              throw new invalid_parameter_exception($debuginfo);
 784          }
 785      }
 786      if (is_array($param) or is_object($param)) {
 787          throw new invalid_parameter_exception($debuginfo);
 788      }
 789  
 790      $cleaned = clean_param($param, $type);
 791  
 792      if ($type == PARAM_FLOAT) {
 793          // Do not detect precision loss here.
 794          if (is_float($param) or is_int($param)) {
 795              // These always fit.
 796          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
 797              throw new invalid_parameter_exception($debuginfo);
 798          }
 799      } else if ((string)$param !== (string)$cleaned) {
 800          // Conversion to string is usually lossless.
 801          throw new invalid_parameter_exception($debuginfo);
 802      }
 803  
 804      return $cleaned;
 805  }
 806  
 807  /**
 808   * Makes sure array contains only the allowed types, this function does not validate array key names!
 809   *
 810   * <code>
 811   * $options = clean_param($options, PARAM_INT);
 812   * </code>
 813   *
 814   * @param array|null $param the variable array we are cleaning
 815   * @param string $type expected format of param after cleaning.
 816   * @param bool $recursive clean recursive arrays
 817   * @return array
 818   * @throws coding_exception
 819   */
 820  function clean_param_array(?array $param, $type, $recursive = false) {
 821      // Convert null to empty array.
 822      $param = (array)$param;
 823      foreach ($param as $key => $value) {
 824          if (is_array($value)) {
 825              if ($recursive) {
 826                  $param[$key] = clean_param_array($value, $type, true);
 827              } else {
 828                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
 829              }
 830          } else {
 831              $param[$key] = clean_param($value, $type);
 832          }
 833      }
 834      return $param;
 835  }
 836  
 837  /**
 838   * Used by {@link optional_param()} and {@link required_param()} to
 839   * clean the variables and/or cast to specific types, based on
 840   * an options field.
 841   * <code>
 842   * $course->format = clean_param($course->format, PARAM_ALPHA);
 843   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
 844   * </code>
 845   *
 846   * @param mixed $param the variable we are cleaning
 847   * @param string $type expected format of param after cleaning.
 848   * @return mixed
 849   * @throws coding_exception
 850   */
 851  function clean_param($param, $type) {
 852      global $CFG;
 853  
 854      if (is_array($param)) {
 855          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
 856      } else if (is_object($param)) {
 857          if (method_exists($param, '__toString')) {
 858              $param = $param->__toString();
 859          } else {
 860              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
 861          }
 862      }
 863  
 864      switch ($type) {
 865          case PARAM_RAW:
 866              // No cleaning at all.
 867              $param = fix_utf8($param);
 868              return $param;
 869  
 870          case PARAM_RAW_TRIMMED:
 871              // No cleaning, but strip leading and trailing whitespace.
 872              $param = (string)fix_utf8($param);
 873              return trim($param);
 874  
 875          case PARAM_CLEAN:
 876              // General HTML cleaning, try to use more specific type if possible this is deprecated!
 877              // Please use more specific type instead.
 878              if (is_numeric($param)) {
 879                  return $param;
 880              }
 881              $param = fix_utf8($param);
 882              // Sweep for scripts, etc.
 883              return clean_text($param);
 884  
 885          case PARAM_CLEANHTML:
 886              // Clean html fragment.
 887              $param = (string)fix_utf8($param);
 888              // Sweep for scripts, etc.
 889              $param = clean_text($param, FORMAT_HTML);
 890              return trim($param);
 891  
 892          case PARAM_INT:
 893              // Convert to integer.
 894              return (int)$param;
 895  
 896          case PARAM_FLOAT:
 897              // Convert to float.
 898              return (float)$param;
 899  
 900          case PARAM_LOCALISEDFLOAT:
 901              // Convert to float.
 902              return unformat_float($param, true);
 903  
 904          case PARAM_ALPHA:
 905              // Remove everything not `a-z`.
 906              return preg_replace('/[^a-zA-Z]/i', '', (string)$param);
 907  
 908          case PARAM_ALPHAEXT:
 909              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
 910              return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param);
 911  
 912          case PARAM_ALPHANUM:
 913              // Remove everything not `a-zA-Z0-9`.
 914              return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param);
 915  
 916          case PARAM_ALPHANUMEXT:
 917              // Remove everything not `a-zA-Z0-9_-`.
 918              return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param);
 919  
 920          case PARAM_SEQUENCE:
 921              // Remove everything not `0-9,`.
 922              return preg_replace('/[^0-9,]/i', '', (string)$param);
 923  
 924          case PARAM_BOOL:
 925              // Convert to 1 or 0.
 926              $tempstr = strtolower((string)$param);
 927              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
 928                  $param = 1;
 929              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
 930                  $param = 0;
 931              } else {
 932                  $param = empty($param) ? 0 : 1;
 933              }
 934              return $param;
 935  
 936          case PARAM_NOTAGS:
 937              // Strip all tags.
 938              $param = fix_utf8($param);
 939              return strip_tags((string)$param);
 940  
 941          case PARAM_TEXT:
 942              // Leave only tags needed for multilang.
 943              $param = fix_utf8($param);
 944              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
 945              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
 946              do {
 947                  if (strpos((string)$param, '</lang>') !== false) {
 948                      // Old and future mutilang syntax.
 949                      $param = strip_tags($param, '<lang>');
 950                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 951                          break;
 952                      }
 953                      $open = false;
 954                      foreach ($matches[0] as $match) {
 955                          if ($match === '</lang>') {
 956                              if ($open) {
 957                                  $open = false;
 958                                  continue;
 959                              } else {
 960                                  break 2;
 961                              }
 962                          }
 963                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
 964                              break 2;
 965                          } else {
 966                              $open = true;
 967                          }
 968                      }
 969                      if ($open) {
 970                          break;
 971                      }
 972                      return $param;
 973  
 974                  } else if (strpos((string)$param, '</span>') !== false) {
 975                      // Current problematic multilang syntax.
 976                      $param = strip_tags($param, '<span>');
 977                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 978                          break;
 979                      }
 980                      $open = false;
 981                      foreach ($matches[0] as $match) {
 982                          if ($match === '</span>') {
 983                              if ($open) {
 984                                  $open = false;
 985                                  continue;
 986                              } else {
 987                                  break 2;
 988                              }
 989                          }
 990                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
 991                              break 2;
 992                          } else {
 993                              $open = true;
 994                          }
 995                      }
 996                      if ($open) {
 997                          break;
 998                      }
 999                      return $param;
1000                  }
1001              } while (false);
1002              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
1003              return strip_tags((string)$param);
1004  
1005          case PARAM_COMPONENT:
1006              // We do not want any guessing here, either the name is correct or not
1007              // please note only normalised component names are accepted.
1008              $param = (string)$param;
1009              if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
1010                  return '';
1011              }
1012              if (strpos($param, '__') !== false) {
1013                  return '';
1014              }
1015              if (strpos($param, 'mod_') === 0) {
1016                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
1017                  if (substr_count($param, '_') != 1) {
1018                      return '';
1019                  }
1020              }
1021              return $param;
1022  
1023          case PARAM_PLUGIN:
1024          case PARAM_AREA:
1025              // We do not want any guessing here, either the name is correct or not.
1026              if (!is_valid_plugin_name($param)) {
1027                  return '';
1028              }
1029              return $param;
1030  
1031          case PARAM_SAFEDIR:
1032              // Remove everything not a-zA-Z0-9_- .
1033              return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param);
1034  
1035          case PARAM_SAFEPATH:
1036              // Remove everything not a-zA-Z0-9/_- .
1037              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param);
1038  
1039          case PARAM_FILE:
1040              // Strip all suspicious characters from filename.
1041              $param = (string)fix_utf8($param);
1042              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
1043              if ($param === '.' || $param === '..') {
1044                  $param = '';
1045              }
1046              return $param;
1047  
1048          case PARAM_PATH:
1049              // Strip all suspicious characters from file path.
1050              $param = (string)fix_utf8($param);
1051              $param = str_replace('\\', '/', $param);
1052  
1053              // Explode the path and clean each element using the PARAM_FILE rules.
1054              $breadcrumb = explode('/', $param);
1055              foreach ($breadcrumb as $key => $crumb) {
1056                  if ($crumb === '.' && $key === 0) {
1057                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
1058                  } else {
1059                      $crumb = clean_param($crumb, PARAM_FILE);
1060                  }
1061                  $breadcrumb[$key] = $crumb;
1062              }
1063              $param = implode('/', $breadcrumb);
1064  
1065              // Remove multiple current path (./././) and multiple slashes (///).
1066              $param = preg_replace('~//+~', '/', $param);
1067              $param = preg_replace('~/(\./)+~', '/', $param);
1068              return $param;
1069  
1070          case PARAM_HOST:
1071              // Allow FQDN or IPv4 dotted quad.
1072              $param = preg_replace('/[^\.\d\w-]/', '', (string)$param );
1073              // Match ipv4 dotted quad.
1074              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1075                  // Confirm values are ok.
1076                  if ( $match[0] > 255
1077                       || $match[1] > 255
1078                       || $match[3] > 255
1079                       || $match[4] > 255 ) {
1080                      // Hmmm, what kind of dotted quad is this?
1081                      $param = '';
1082                  }
1083              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1084                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1085                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1086                         ) {
1087                  // All is ok - $param is respected.
1088              } else {
1089                  // All is not ok...
1090                  $param='';
1091              }
1092              return $param;
1093  
1094          case PARAM_URL:
1095              // Allow safe urls.
1096              $param = (string)fix_utf8($param);
1097              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1098              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1099                  // All is ok, param is respected.
1100              } else {
1101                  // Not really ok.
1102                  $param ='';
1103              }
1104              return $param;
1105  
1106          case PARAM_LOCALURL:
1107              // Allow http absolute, root relative and relative URLs within wwwroot.
1108              $param = clean_param($param, PARAM_URL);
1109              if (!empty($param)) {
1110  
1111                  if ($param === $CFG->wwwroot) {
1112                      // Exact match;
1113                  } else if (preg_match(':^/:', $param)) {
1114                      // Root-relative, ok!
1115                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1116                      // Absolute, and matches our wwwroot.
1117                  } else {
1118  
1119                      // Relative - let's make sure there are no tricks.
1120                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
1121                          // Looks ok.
1122                      } else {
1123                          $param = '';
1124                      }
1125                  }
1126              }
1127              return $param;
1128  
1129          case PARAM_PEM:
1130              $param = trim((string)$param);
1131              // PEM formatted strings may contain letters/numbers and the symbols:
1132              //   forward slash: /
1133              //   plus sign:     +
1134              //   equal sign:    =
1135              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1136              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1137                  list($wholething, $body) = $matches;
1138                  unset($wholething, $matches);
1139                  $b64 = clean_param($body, PARAM_BASE64);
1140                  if (!empty($b64)) {
1141                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1142                  } else {
1143                      return '';
1144                  }
1145              }
1146              return '';
1147  
1148          case PARAM_BASE64:
1149              if (!empty($param)) {
1150                  // PEM formatted strings may contain letters/numbers and the symbols
1151                  //   forward slash: /
1152                  //   plus sign:     +
1153                  //   equal sign:    =.
1154                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1155                      return '';
1156                  }
1157                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1158                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1159                  // than (or equal to) 64 characters long.
1160                  for ($i=0, $j=count($lines); $i < $j; $i++) {
1161                      if ($i + 1 == $j) {
1162                          if (64 < strlen($lines[$i])) {
1163                              return '';
1164                          }
1165                          continue;
1166                      }
1167  
1168                      if (64 != strlen($lines[$i])) {
1169                          return '';
1170                      }
1171                  }
1172                  return implode("\n", $lines);
1173              } else {
1174                  return '';
1175              }
1176  
1177          case PARAM_TAG:
1178              $param = (string)fix_utf8($param);
1179              // Please note it is not safe to use the tag name directly anywhere,
1180              // it must be processed with s(), urlencode() before embedding anywhere.
1181              // Remove some nasties.
1182              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1183              // Convert many whitespace chars into one.
1184              $param = preg_replace('/\s+/u', ' ', $param);
1185              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1186              return $param;
1187  
1188          case PARAM_TAGLIST:
1189              $param = (string)fix_utf8($param);
1190              $tags = explode(',', $param);
1191              $result = array();
1192              foreach ($tags as $tag) {
1193                  $res = clean_param($tag, PARAM_TAG);
1194                  if ($res !== '') {
1195                      $result[] = $res;
1196                  }
1197              }
1198              if ($result) {
1199                  return implode(',', $result);
1200              } else {
1201                  return '';
1202              }
1203  
1204          case PARAM_CAPABILITY:
1205              if (get_capability_info($param)) {
1206                  return $param;
1207              } else {
1208                  return '';
1209              }
1210  
1211          case PARAM_PERMISSION:
1212              $param = (int)$param;
1213              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1214                  return $param;
1215              } else {
1216                  return CAP_INHERIT;
1217              }
1218  
1219          case PARAM_AUTH:
1220              $param = clean_param($param, PARAM_PLUGIN);
1221              if (empty($param)) {
1222                  return '';
1223              } else if (exists_auth_plugin($param)) {
1224                  return $param;
1225              } else {
1226                  return '';
1227              }
1228  
1229          case PARAM_LANG:
1230              $param = clean_param($param, PARAM_SAFEDIR);
1231              if (get_string_manager()->translation_exists($param)) {
1232                  return $param;
1233              } else {
1234                  // Specified language is not installed or param malformed.
1235                  return '';
1236              }
1237  
1238          case PARAM_THEME:
1239              $param = clean_param($param, PARAM_PLUGIN);
1240              if (empty($param)) {
1241                  return '';
1242              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1243                  return $param;
1244              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1245                  return $param;
1246              } else {
1247                  // Specified theme is not installed.
1248                  return '';
1249              }
1250  
1251          case PARAM_USERNAME:
1252              $param = (string)fix_utf8($param);
1253              $param = trim($param);
1254              // Convert uppercase to lowercase MDL-16919.
1255              $param = core_text::strtolower($param);
1256              if (empty($CFG->extendedusernamechars)) {
1257                  $param = str_replace(" " , "", $param);
1258                  // Regular expression, eliminate all chars EXCEPT:
1259                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1260                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1261              }
1262              return $param;
1263  
1264          case PARAM_EMAIL:
1265              $param = fix_utf8($param);
1266              if (validate_email($param ?? '')) {
1267                  return $param;
1268              } else {
1269                  return '';
1270              }
1271  
1272          case PARAM_STRINGID:
1273              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) {
1274                  return $param;
1275              } else {
1276                  return '';
1277              }
1278  
1279          case PARAM_TIMEZONE:
1280              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1281              $param = (string)fix_utf8($param);
1282              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1283              if (preg_match($timezonepattern, $param)) {
1284                  return $param;
1285              } else {
1286                  return '';
1287              }
1288  
1289          default:
1290              // Doh! throw error, switched parameters in optional_param or another serious problem.
1291              throw new \moodle_exception("unknownparamtype", '', '', $type);
1292      }
1293  }
1294  
1295  /**
1296   * Whether the PARAM_* type is compatible in RTL.
1297   *
1298   * Being compatible with RTL means that the data they contain can flow
1299   * from right-to-left or left-to-right without compromising the user experience.
1300   *
1301   * Take URLs for example, they are not RTL compatible as they should always
1302   * flow from the left to the right. This also applies to numbers, email addresses,
1303   * configuration snippets, base64 strings, etc...
1304   *
1305   * This function tries to best guess which parameters can contain localised strings.
1306   *
1307   * @param string $paramtype Constant PARAM_*.
1308   * @return bool
1309   */
1310  function is_rtl_compatible($paramtype) {
1311      return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1312  }
1313  
1314  /**
1315   * Makes sure the data is using valid utf8, invalid characters are discarded.
1316   *
1317   * Note: this function is not intended for full objects with methods and private properties.
1318   *
1319   * @param mixed $value
1320   * @return mixed with proper utf-8 encoding
1321   */
1322  function fix_utf8($value) {
1323      if (is_null($value) or $value === '') {
1324          return $value;
1325  
1326      } else if (is_string($value)) {
1327          if ((string)(int)$value === $value) {
1328              // Shortcut.
1329              return $value;
1330          }
1331  
1332          // Remove null bytes or invalid Unicode sequences from value.
1333          $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
1334  
1335          // Note: this duplicates min_fix_utf8() intentionally.
1336          static $buggyiconv = null;
1337          if ($buggyiconv === null) {
1338              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1339          }
1340  
1341          if ($buggyiconv) {
1342              if (function_exists('mb_convert_encoding')) {
1343                  $subst = mb_substitute_character();
1344                  mb_substitute_character('none');
1345                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1346                  mb_substitute_character($subst);
1347  
1348              } else {
1349                  // Warn admins on admin/index.php page.
1350                  $result = $value;
1351              }
1352  
1353          } else {
1354              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1355          }
1356  
1357          return $result;
1358  
1359      } else if (is_array($value)) {
1360          foreach ($value as $k => $v) {
1361              $value[$k] = fix_utf8($v);
1362          }
1363          return $value;
1364  
1365      } else if (is_object($value)) {
1366          // Do not modify original.
1367          $value = clone($value);
1368          foreach ($value as $k => $v) {
1369              $value->$k = fix_utf8($v);
1370          }
1371          return $value;
1372  
1373      } else {
1374          // This is some other type, no utf-8 here.
1375          return $value;
1376      }
1377  }
1378  
1379  /**
1380   * Return true if given value is integer or string with integer value
1381   *
1382   * @param mixed $value String or Int
1383   * @return bool true if number, false if not
1384   */
1385  function is_number($value) {
1386      if (is_int($value)) {
1387          return true;
1388      } else if (is_string($value)) {
1389          return ((string)(int)$value) === $value;
1390      } else {
1391          return false;
1392      }
1393  }
1394  
1395  /**
1396   * Returns host part from url.
1397   *
1398   * @param string $url full url
1399   * @return string host, null if not found
1400   */
1401  function get_host_from_url($url) {
1402      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1403      if ($matches) {
1404          return $matches[1];
1405      }
1406      return null;
1407  }
1408  
1409  /**
1410   * Tests whether anything was returned by text editor
1411   *
1412   * This function is useful for testing whether something you got back from
1413   * the HTML editor actually contains anything. Sometimes the HTML editor
1414   * appear to be empty, but actually you get back a <br> tag or something.
1415   *
1416   * @param string $string a string containing HTML.
1417   * @return boolean does the string contain any actual content - that is text,
1418   * images, objects, etc.
1419   */
1420  function html_is_blank($string) {
1421      return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
1422  }
1423  
1424  /**
1425   * Set a key in global configuration
1426   *
1427   * Set a key/value pair in both this session's {@link $CFG} global variable
1428   * and in the 'config' database table for future sessions.
1429   *
1430   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1431   * In that case it doesn't affect $CFG.
1432   *
1433   * A NULL value will delete the entry.
1434   *
1435   * NOTE: this function is called from lib/db/upgrade.php
1436   *
1437   * @param string $name the key to set
1438   * @param string $value the value to set (without magic quotes)
1439   * @param string $plugin (optional) the plugin scope, default null
1440   * @return bool true or exception
1441   */
1442  function set_config($name, $value, $plugin = null) {
1443      global $CFG, $DB;
1444  
1445      // Redirect to appropriate handler when value is null.
1446      if ($value === null) {
1447          return unset_config($name, $plugin);
1448      }
1449  
1450      // Set variables determining conditions and where to store the new config.
1451      // Plugin config goes to {config_plugins}, core config goes to {config}.
1452      $iscore = empty($plugin);
1453      if ($iscore) {
1454          // If it's for core config.
1455          $table = 'config';
1456          $conditions = ['name' => $name];
1457          $invalidatecachekey = 'core';
1458      } else {
1459          // If it's a plugin.
1460          $table = 'config_plugins';
1461          $conditions = ['name' => $name, 'plugin' => $plugin];
1462          $invalidatecachekey = $plugin;
1463      }
1464  
1465      // DB handling - checks for existing config, updating or inserting only if necessary.
1466      $invalidatecache = true;
1467      $inserted = false;
1468      $record = $DB->get_record($table, $conditions, 'id, value');
1469      if ($record === false) {
1470          // Inserts a new config record.
1471          $config = new stdClass();
1472          $config->name  = $name;
1473          $config->value = $value;
1474          if (!$iscore) {
1475              $config->plugin = $plugin;
1476          }
1477          $inserted = $DB->insert_record($table, $config, false);
1478      } else if ($invalidatecache = ($record->value !== $value)) {
1479          // Record exists - Check and only set new value if it has changed.
1480          $DB->set_field($table, 'value', $value, ['id' => $record->id]);
1481      }
1482  
1483      if ($iscore && !isset($CFG->config_php_settings[$name])) {
1484          // So it's defined for this invocation at least.
1485          // Settings from db are always strings.
1486          $CFG->$name = (string) $value;
1487      }
1488  
1489      // When setting config during a Behat test (in the CLI script, not in the web browser
1490      // requests), remember which ones are set so that we can clear them later.
1491      if ($iscore && $inserted && defined('BEHAT_TEST')) {
1492          $CFG->behat_cli_added_config[$name] = true;
1493      }
1494  
1495      // Update siteidentifier cache, if required.
1496      if ($iscore && $name === 'siteidentifier') {
1497          cache_helper::update_site_identifier($value);
1498      }
1499  
1500      // Invalidate cache, if required.
1501      if ($invalidatecache) {
1502          cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
1503      }
1504  
1505      return true;
1506  }
1507  
1508  /**
1509   * Get configuration values from the global config table
1510   * or the config_plugins table.
1511   *
1512   * If called with one parameter, it will load all the config
1513   * variables for one plugin, and return them as an object.
1514   *
1515   * If called with 2 parameters it will return a string single
1516   * value or false if the value is not found.
1517   *
1518   * NOTE: this function is called from lib/db/upgrade.php
1519   *
1520   * @param string $plugin full component name
1521   * @param string $name default null
1522   * @return mixed hash-like object or single value, return false no config found
1523   * @throws dml_exception
1524   */
1525  function get_config($plugin, $name = null) {
1526      global $CFG, $DB;
1527  
1528      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1529          $forced =& $CFG->config_php_settings;
1530          $iscore = true;
1531          $plugin = 'core';
1532      } else {
1533          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1534              $forced =& $CFG->forced_plugin_settings[$plugin];
1535          } else {
1536              $forced = array();
1537          }
1538          $iscore = false;
1539      }
1540  
1541      if (!isset($CFG->siteidentifier)) {
1542          try {
1543              // This may throw an exception during installation, which is how we detect the
1544              // need to install the database. For more details see {@see initialise_cfg()}.
1545              $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1546          } catch (dml_exception $ex) {
1547              // Set siteidentifier to false. We don't want to trip this continually.
1548              $siteidentifier = false;
1549              throw $ex;
1550          }
1551      }
1552  
1553      if (!empty($name)) {
1554          if (array_key_exists($name, $forced)) {
1555              return (string)$forced[$name];
1556          } else if ($name === 'siteidentifier' && $plugin == 'core') {
1557              return $CFG->siteidentifier;
1558          }
1559      }
1560  
1561      $cache = cache::make('core', 'config');
1562      $result = $cache->get($plugin);
1563      if ($result === false) {
1564          // The user is after a recordset.
1565          if (!$iscore) {
1566              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1567          } else {
1568              // This part is not really used any more, but anyway...
1569              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1570          }
1571          $cache->set($plugin, $result);
1572      }
1573  
1574      if (!empty($name)) {
1575          if (array_key_exists($name, $result)) {
1576              return $result[$name];
1577          }
1578          return false;
1579      }
1580  
1581      if ($plugin === 'core') {
1582          $result['siteidentifier'] = $CFG->siteidentifier;
1583      }
1584  
1585      foreach ($forced as $key => $value) {
1586          if (is_null($value) or is_array($value) or is_object($value)) {
1587              // We do not want any extra mess here, just real settings that could be saved in db.
1588              unset($result[$key]);
1589          } else {
1590              // Convert to string as if it went through the DB.
1591              $result[$key] = (string)$value;
1592          }
1593      }
1594  
1595      return (object)$result;
1596  }
1597  
1598  /**
1599   * Removes a key from global configuration.
1600   *
1601   * NOTE: this function is called from lib/db/upgrade.php
1602   *
1603   * @param string $name the key to set
1604   * @param string $plugin (optional) the plugin scope
1605   * @return boolean whether the operation succeeded.
1606   */
1607  function unset_config($name, $plugin=null) {
1608      global $CFG, $DB;
1609  
1610      if (empty($plugin)) {
1611          unset($CFG->$name);
1612          $DB->delete_records('config', array('name' => $name));
1613          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1614      } else {
1615          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1616          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1617      }
1618  
1619      return true;
1620  }
1621  
1622  /**
1623   * Remove all the config variables for a given plugin.
1624   *
1625   * NOTE: this function is called from lib/db/upgrade.php
1626   *
1627   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1628   * @return boolean whether the operation succeeded.
1629   */
1630  function unset_all_config_for_plugin($plugin) {
1631      global $DB;
1632      // Delete from the obvious config_plugins first.
1633      $DB->delete_records('config_plugins', array('plugin' => $plugin));
1634      // Next delete any suspect settings from config.
1635      $like = $DB->sql_like('name', '?', true, true, false, '|');
1636      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1637      $DB->delete_records_select('config', $like, $params);
1638      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1639      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1640  
1641      return true;
1642  }
1643  
1644  /**
1645   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1646   *
1647   * All users are verified if they still have the necessary capability.
1648   *
1649   * @param string $value the value of the config setting.
1650   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1651   * @param bool $includeadmins include administrators.
1652   * @return array of user objects.
1653   */
1654  function get_users_from_config($value, $capability, $includeadmins = true) {
1655      if (empty($value) or $value === '$@NONE@$') {
1656          return array();
1657      }
1658  
1659      // We have to make sure that users still have the necessary capability,
1660      // it should be faster to fetch them all first and then test if they are present
1661      // instead of validating them one-by-one.
1662      $users = get_users_by_capability(context_system::instance(), $capability);
1663      if ($includeadmins) {
1664          $admins = get_admins();
1665          foreach ($admins as $admin) {
1666              $users[$admin->id] = $admin;
1667          }
1668      }
1669  
1670      if ($value === '$@ALL@$') {
1671          return $users;
1672      }
1673  
1674      $result = array(); // Result in correct order.
1675      $allowed = explode(',', $value);
1676      foreach ($allowed as $uid) {
1677          if (isset($users[$uid])) {
1678              $user = $users[$uid];
1679              $result[$user->id] = $user;
1680          }
1681      }
1682  
1683      return $result;
1684  }
1685  
1686  
1687  /**
1688   * Invalidates browser caches and cached data in temp.
1689   *
1690   * @return void
1691   */
1692  function purge_all_caches() {
1693      purge_caches();
1694  }
1695  
1696  /**
1697   * Selectively invalidate different types of cache.
1698   *
1699   * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1700   * areas alone or in combination.
1701   *
1702   * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1703   *        'muc'    Purge MUC caches?
1704   *        'theme'  Purge theme cache?
1705   *        'lang'   Purge language string cache?
1706   *        'js'     Purge javascript cache?
1707   *        'filter' Purge text filter cache?
1708   *        'other'  Purge all other caches?
1709   */
1710  function purge_caches($options = []) {
1711      $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1712      if (empty(array_filter($options))) {
1713          $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1714      } else {
1715          $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1716      }
1717      if ($options['muc']) {
1718          cache_helper::purge_all();
1719      }
1720      if ($options['theme']) {
1721          theme_reset_all_caches();
1722      }
1723      if ($options['lang']) {
1724          get_string_manager()->reset_caches();
1725      }
1726      if ($options['js']) {
1727          js_reset_all_caches();
1728      }
1729      if ($options['template']) {
1730          template_reset_all_caches();
1731      }
1732      if ($options['filter']) {
1733          reset_text_filters_cache();
1734      }
1735      if ($options['other']) {
1736          purge_other_caches();
1737      }
1738  }
1739  
1740  /**
1741   * Purge all non-MUC caches not otherwise purged in purge_caches.
1742   *
1743   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1744   * {@link phpunit_util::reset_dataroot()}
1745   */
1746  function purge_other_caches() {
1747      global $DB, $CFG;
1748      if (class_exists('core_plugin_manager')) {
1749          core_plugin_manager::reset_caches();
1750      }
1751  
1752      // Bump up cacherev field for all courses.
1753      try {
1754          increment_revision_number('course', 'cacherev', '');
1755      } catch (moodle_exception $e) {
1756          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1757      }
1758  
1759      $DB->reset_caches();
1760  
1761      // Purge all other caches: rss, simplepie, etc.
1762      clearstatcache();
1763      remove_dir($CFG->cachedir.'', true);
1764  
1765      // Make sure cache dir is writable, throws exception if not.
1766      make_cache_directory('');
1767  
1768      // This is the only place where we purge local caches, we are only adding files there.
1769      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1770      remove_dir($CFG->localcachedir, true);
1771      set_config('localcachedirpurged', time());
1772      make_localcache_directory('', true);
1773      \core\task\manager::clear_static_caches();
1774  }
1775  
1776  /**
1777   * Get volatile flags
1778   *
1779   * @param string $type
1780   * @param int $changedsince default null
1781   * @return array records array
1782   */
1783  function get_cache_flags($type, $changedsince = null) {
1784      global $DB;
1785  
1786      $params = array('type' => $type, 'expiry' => time());
1787      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1788      if ($changedsince !== null) {
1789          $params['changedsince'] = $changedsince;
1790          $sqlwhere .= " AND timemodified > :changedsince";
1791      }
1792      $cf = array();
1793      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1794          foreach ($flags as $flag) {
1795              $cf[$flag->name] = $flag->value;
1796          }
1797      }
1798      return $cf;
1799  }
1800  
1801  /**
1802   * Get volatile flags
1803   *
1804   * @param string $type
1805   * @param string $name
1806   * @param int $changedsince default null
1807   * @return string|false The cache flag value or false
1808   */
1809  function get_cache_flag($type, $name, $changedsince=null) {
1810      global $DB;
1811  
1812      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1813  
1814      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1815      if ($changedsince !== null) {
1816          $params['changedsince'] = $changedsince;
1817          $sqlwhere .= " AND timemodified > :changedsince";
1818      }
1819  
1820      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1821  }
1822  
1823  /**
1824   * Set a volatile flag
1825   *
1826   * @param string $type the "type" namespace for the key
1827   * @param string $name the key to set
1828   * @param string $value the value to set (without magic quotes) - null will remove the flag
1829   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1830   * @return bool Always returns true
1831   */
1832  function set_cache_flag($type, $name, $value, $expiry = null) {
1833      global $DB;
1834  
1835      $timemodified = time();
1836      if ($expiry === null || $expiry < $timemodified) {
1837          $expiry = $timemodified + 24 * 60 * 60;
1838      } else {
1839          $expiry = (int)$expiry;
1840      }
1841  
1842      if ($value === null) {
1843          unset_cache_flag($type, $name);
1844          return true;
1845      }
1846  
1847      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1848          // This is a potential problem in DEBUG_DEVELOPER.
1849          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1850              return true; // No need to update.
1851          }
1852          $f->value        = $value;
1853          $f->expiry       = $expiry;
1854          $f->timemodified = $timemodified;
1855          $DB->update_record('cache_flags', $f);
1856      } else {
1857          $f = new stdClass();
1858          $f->flagtype     = $type;
1859          $f->name         = $name;
1860          $f->value        = $value;
1861          $f->expiry       = $expiry;
1862          $f->timemodified = $timemodified;
1863          $DB->insert_record('cache_flags', $f);
1864      }
1865      return true;
1866  }
1867  
1868  /**
1869   * Removes a single volatile flag
1870   *
1871   * @param string $type the "type" namespace for the key
1872   * @param string $name the key to set
1873   * @return bool
1874   */
1875  function unset_cache_flag($type, $name) {
1876      global $DB;
1877      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1878      return true;
1879  }
1880  
1881  /**
1882   * Garbage-collect volatile flags
1883   *
1884   * @return bool Always returns true
1885   */
1886  function gc_cache_flags() {
1887      global $DB;
1888      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1889      return true;
1890  }
1891  
1892  // USER PREFERENCE API.
1893  
1894  /**
1895   * Refresh user preference cache. This is used most often for $USER
1896   * object that is stored in session, but it also helps with performance in cron script.
1897   *
1898   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1899   *
1900   * @package  core
1901   * @category preference
1902   * @access   public
1903   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1904   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1905   * @throws   coding_exception
1906   * @return   null
1907   */
1908  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1909      global $DB;
1910      // Static cache, we need to check on each page load, not only every 2 minutes.
1911      static $loadedusers = array();
1912  
1913      if (!isset($user->id)) {
1914          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1915      }
1916  
1917      if (empty($user->id) or isguestuser($user->id)) {
1918          // No permanent storage for not-logged-in users and guest.
1919          if (!isset($user->preference)) {
1920              $user->preference = array();
1921          }
1922          return;
1923      }
1924  
1925      $timenow = time();
1926  
1927      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1928          // Already loaded at least once on this page. Are we up to date?
1929          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1930              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1931              return;
1932  
1933          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1934              // No change since the lastcheck on this page.
1935              $user->preference['_lastloaded'] = $timenow;
1936              return;
1937          }
1938      }
1939  
1940      // OK, so we have to reload all preferences.
1941      $loadedusers[$user->id] = true;
1942      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1943      $user->preference['_lastloaded'] = $timenow;
1944  }
1945  
1946  /**
1947   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1948   *
1949   * NOTE: internal function, do not call from other code.
1950   *
1951   * @package core
1952   * @access private
1953   * @param integer $userid the user whose prefs were changed.
1954   */
1955  function mark_user_preferences_changed($userid) {
1956      global $CFG;
1957  
1958      if (empty($userid) or isguestuser($userid)) {
1959          // No cache flags for guest and not-logged-in users.
1960          return;
1961      }
1962  
1963      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1964  }
1965  
1966  /**
1967   * Sets a preference for the specified user.
1968   *
1969   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1970   *
1971   * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1972   *
1973   * @package  core
1974   * @category preference
1975   * @access   public
1976   * @param    string            $name  The key to set as preference for the specified user
1977   * @param    string            $value The value to set for the $name key in the specified user's
1978   *                                    record, null means delete current value.
1979   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1980   * @throws   coding_exception
1981   * @return   bool                     Always true or exception
1982   */
1983  function set_user_preference($name, $value, $user = null) {
1984      global $USER, $DB;
1985  
1986      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1987          throw new coding_exception('Invalid preference name in set_user_preference() call');
1988      }
1989  
1990      if (is_null($value)) {
1991          // Null means delete current.
1992          return unset_user_preference($name, $user);
1993      } else if (is_object($value)) {
1994          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1995      } else if (is_array($value)) {
1996          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1997      }
1998      // Value column maximum length is 1333 characters.
1999      $value = (string)$value;
2000      if (core_text::strlen($value) > 1333) {
2001          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
2002      }
2003  
2004      if (is_null($user)) {
2005          $user = $USER;
2006      } else if (isset($user->id)) {
2007          // It is a valid object.
2008      } else if (is_numeric($user)) {
2009          $user = (object)array('id' => (int)$user);
2010      } else {
2011          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
2012      }
2013  
2014      check_user_preferences_loaded($user);
2015  
2016      if (empty($user->id) or isguestuser($user->id)) {
2017          // No permanent storage for not-logged-in users and guest.
2018          $user->preference[$name] = $value;
2019          return true;
2020      }
2021  
2022      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
2023          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
2024              // Preference already set to this value.
2025              return true;
2026          }
2027          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
2028  
2029      } else {
2030          $preference = new stdClass();
2031          $preference->userid = $user->id;
2032          $preference->name   = $name;
2033          $preference->value  = $value;
2034          $DB->insert_record('user_preferences', $preference);
2035      }
2036  
2037      // Update value in cache.
2038      $user->preference[$name] = $value;
2039      // Update the $USER in case where we've not a direct reference to $USER.
2040      if ($user !== $USER && $user->id == $USER->id) {
2041          $USER->preference[$name] = $value;
2042      }
2043  
2044      // Set reload flag for other sessions.
2045      mark_user_preferences_changed($user->id);
2046  
2047      return true;
2048  }
2049  
2050  /**
2051   * Sets a whole array of preferences for the current user
2052   *
2053   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2054   *
2055   * @package  core
2056   * @category preference
2057   * @access   public
2058   * @param    array             $prefarray An array of key/value pairs to be set
2059   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
2060   * @return   bool                         Always true or exception
2061   */
2062  function set_user_preferences(array $prefarray, $user = null) {
2063      foreach ($prefarray as $name => $value) {
2064          set_user_preference($name, $value, $user);
2065      }
2066      return true;
2067  }
2068  
2069  /**
2070   * Unsets a preference completely by deleting it from the database
2071   *
2072   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2073   *
2074   * @package  core
2075   * @category preference
2076   * @access   public
2077   * @param    string            $name The key to unset as preference for the specified user
2078   * @param    stdClass|int|null $user A moodle user object or id, null means current user
2079   * @throws   coding_exception
2080   * @return   bool                    Always true or exception
2081   */
2082  function unset_user_preference($name, $user = null) {
2083      global $USER, $DB;
2084  
2085      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2086          throw new coding_exception('Invalid preference name in unset_user_preference() call');
2087      }
2088  
2089      if (is_null($user)) {
2090          $user = $USER;
2091      } else if (isset($user->id)) {
2092          // It is a valid object.
2093      } else if (is_numeric($user)) {
2094          $user = (object)array('id' => (int)$user);
2095      } else {
2096          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2097      }
2098  
2099      check_user_preferences_loaded($user);
2100  
2101      if (empty($user->id) or isguestuser($user->id)) {
2102          // No permanent storage for not-logged-in user and guest.
2103          unset($user->preference[$name]);
2104          return true;
2105      }
2106  
2107      // Delete from DB.
2108      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2109  
2110      // Delete the preference from cache.
2111      unset($user->preference[$name]);
2112      // Update the $USER in case where we've not a direct reference to $USER.
2113      if ($user !== $USER && $user->id == $USER->id) {
2114          unset($USER->preference[$name]);
2115      }
2116  
2117      // Set reload flag for other sessions.
2118      mark_user_preferences_changed($user->id);
2119  
2120      return true;
2121  }
2122  
2123  /**
2124   * Used to fetch user preference(s)
2125   *
2126   * If no arguments are supplied this function will return
2127   * all of the current user preferences as an array.
2128   *
2129   * If a name is specified then this function
2130   * attempts to return that particular preference value.  If
2131   * none is found, then the optional value $default is returned,
2132   * otherwise null.
2133   *
2134   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2135   *
2136   * @package  core
2137   * @category preference
2138   * @access   public
2139   * @param    string            $name    Name of the key to use in finding a preference value
2140   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
2141   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
2142   * @throws   coding_exception
2143   * @return   string|mixed|null          A string containing the value of a single preference. An
2144   *                                      array with all of the preferences or null
2145   */
2146  function get_user_preferences($name = null, $default = null, $user = null) {
2147      global $USER;
2148  
2149      if (is_null($name)) {
2150          // All prefs.
2151      } else if (is_numeric($name) or $name === '_lastloaded') {
2152          throw new coding_exception('Invalid preference name in get_user_preferences() call');
2153      }
2154  
2155      if (is_null($user)) {
2156          $user = $USER;
2157      } else if (isset($user->id)) {
2158          // Is a valid object.
2159      } else if (is_numeric($user)) {
2160          if ($USER->id == $user) {
2161              $user = $USER;
2162          } else {
2163              $user = (object)array('id' => (int)$user);
2164          }
2165      } else {
2166          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2167      }
2168  
2169      check_user_preferences_loaded($user);
2170  
2171      if (empty($name)) {
2172          // All values.
2173          return $user->preference;
2174      } else if (isset($user->preference[$name])) {
2175          // The single string value.
2176          return $user->preference[$name];
2177      } else {
2178          // Default value (null if not specified).
2179          return $default;
2180      }
2181  }
2182  
2183  // FUNCTIONS FOR HANDLING TIME.
2184  
2185  /**
2186   * Given Gregorian date parts in user time produce a GMT timestamp.
2187   *
2188   * @package core
2189   * @category time
2190   * @param int $year The year part to create timestamp of
2191   * @param int $month The month part to create timestamp of
2192   * @param int $day The day part to create timestamp of
2193   * @param int $hour The hour part to create timestamp of
2194   * @param int $minute The minute part to create timestamp of
2195   * @param int $second The second part to create timestamp of
2196   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2197   *             if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2198   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2199   *             applied only if timezone is 99 or string.
2200   * @return int GMT timestamp
2201   */
2202  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2203      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2204      $date->setDate((int)$year, (int)$month, (int)$day);
2205      $date->setTime((int)$hour, (int)$minute, (int)$second);
2206  
2207      $time = $date->getTimestamp();
2208  
2209      if ($time === false) {
2210          throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2211              ' This can fail if year is more than 2038 and OS is 32 bit windows');
2212      }
2213  
2214      // Moodle BC DST stuff.
2215      if (!$applydst) {
2216          $time += dst_offset_on($time, $timezone);
2217      }
2218  
2219      return $time;
2220  
2221  }
2222  
2223  /**
2224   * Format a date/time (seconds) as weeks, days, hours etc as needed
2225   *
2226   * Given an amount of time in seconds, returns string
2227   * formatted nicely as years, days, hours etc as needed
2228   *
2229   * @package core
2230   * @category time
2231   * @uses MINSECS
2232   * @uses HOURSECS
2233   * @uses DAYSECS
2234   * @uses YEARSECS
2235   * @param int $totalsecs Time in seconds
2236   * @param stdClass $str Should be a time object
2237   * @return string A nicely formatted date/time string
2238   */
2239  function format_time($totalsecs, $str = null) {
2240  
2241      $totalsecs = abs($totalsecs);
2242  
2243      if (!$str) {
2244          // Create the str structure the slow way.
2245          $str = new stdClass();
2246          $str->day   = get_string('day');
2247          $str->days  = get_string('days');
2248          $str->hour  = get_string('hour');
2249          $str->hours = get_string('hours');
2250          $str->min   = get_string('min');
2251          $str->mins  = get_string('mins');
2252          $str->sec   = get_string('sec');
2253          $str->secs  = get_string('secs');
2254          $str->year  = get_string('year');
2255          $str->years = get_string('years');
2256      }
2257  
2258      $years     = floor($totalsecs/YEARSECS);
2259      $remainder = $totalsecs - ($years*YEARSECS);
2260      $days      = floor($remainder/DAYSECS);
2261      $remainder = $totalsecs - ($days*DAYSECS);
2262      $hours     = floor($remainder/HOURSECS);
2263      $remainder = $remainder - ($hours*HOURSECS);
2264      $mins      = floor($remainder/MINSECS);
2265      $secs      = $remainder - ($mins*MINSECS);
2266  
2267      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2268      $sm = ($mins == 1)  ? $str->min  : $str->mins;
2269      $sh = ($hours == 1) ? $str->hour : $str->hours;
2270      $sd = ($days == 1)  ? $str->day  : $str->days;
2271      $sy = ($years == 1)  ? $str->year  : $str->years;
2272  
2273      $oyears = '';
2274      $odays = '';
2275      $ohours = '';
2276      $omins = '';
2277      $osecs = '';
2278  
2279      if ($years) {
2280          $oyears  = $years .' '. $sy;
2281      }
2282      if ($days) {
2283          $odays  = $days .' '. $sd;
2284      }
2285      if ($hours) {
2286          $ohours = $hours .' '. $sh;
2287      }
2288      if ($mins) {
2289          $omins  = $mins .' '. $sm;
2290      }
2291      if ($secs) {
2292          $osecs  = $secs .' '. $ss;
2293      }
2294  
2295      if ($years) {
2296          return trim($oyears .' '. $odays);
2297      }
2298      if ($days) {
2299          return trim($odays .' '. $ohours);
2300      }
2301      if ($hours) {
2302          return trim($ohours .' '. $omins);
2303      }
2304      if ($mins) {
2305          return trim($omins .' '. $osecs);
2306      }
2307      if ($secs) {
2308          return $osecs;
2309      }
2310      return get_string('now');
2311  }
2312  
2313  /**
2314   * Returns a formatted string that represents a date in user time.
2315   *
2316   * @package core
2317   * @category time
2318   * @param int $date the timestamp in UTC, as obtained from the database.
2319   * @param string $format strftime format. You should probably get this using
2320   *        get_string('strftime...', 'langconfig');
2321   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2322   *        not 99 then daylight saving will not be added.
2323   *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2324   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2325   *        If false then the leading zero is maintained.
2326   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2327   * @return string the formatted date/time.
2328   */
2329  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2330      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2331      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2332  }
2333  
2334  /**
2335   * Returns a html "time" tag with both the exact user date with timezone information
2336   * as a datetime attribute in the W3C format, and the user readable date and time as text.
2337   *
2338   * @package core
2339   * @category time
2340   * @param int $date the timestamp in UTC, as obtained from the database.
2341   * @param string $format strftime format. You should probably get this using
2342   *        get_string('strftime...', 'langconfig');
2343   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2344   *        not 99 then daylight saving will not be added.
2345   *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2346   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2347   *        If false then the leading zero is maintained.
2348   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2349   * @return string the formatted date/time.
2350   */
2351  function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2352      $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2353      if (CLI_SCRIPT && !PHPUNIT_TEST) {
2354          return $userdatestr;
2355      }
2356      $machinedate = new DateTime();
2357      $machinedate->setTimestamp(intval($date));
2358      $machinedate->setTimezone(core_date::get_user_timezone_object());
2359  
2360      return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2361  }
2362  
2363  /**
2364   * Returns a formatted date ensuring it is UTF-8.
2365   *
2366   * If we are running under Windows convert to Windows encoding and then back to UTF-8
2367   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2368   *
2369   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2370   * @param string $format strftime format.
2371   * @param int|float|string $tz the user timezone
2372   * @return string the formatted date/time.
2373   * @since Moodle 2.3.3
2374   */
2375  function date_format_string($date, $format, $tz = 99) {
2376  
2377      date_default_timezone_set(core_date::get_user_timezone($tz));
2378  
2379      if (date('A', 0) === date('A', HOURSECS * 18)) {
2380          $datearray = getdate($date);
2381          $format = str_replace([
2382              '%P',
2383              '%p',
2384          ], [
2385              $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
2386              $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
2387          ], $format);
2388      }
2389  
2390      $datestring = core_date::strftime($format, $date);
2391      core_date::set_default_server_timezone();
2392  
2393      return $datestring;
2394  }
2395  
2396  /**
2397   * Given a $time timestamp in GMT (seconds since epoch),
2398   * returns an array that represents the Gregorian date in user time
2399   *
2400   * @package core
2401   * @category time
2402   * @param int $time Timestamp in GMT
2403   * @param float|int|string $timezone user timezone
2404   * @return array An array that represents the date in user time
2405   */
2406  function usergetdate($time, $timezone=99) {
2407      if ($time === null) {
2408          // PHP8 and PHP7 return different results when getdate(null) is called.
2409          // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
2410          // In the future versions of Moodle we may consider adding a strict typehint.
2411          debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
2412          $time = 0;
2413      }
2414  
2415      date_default_timezone_set(core_date::get_user_timezone($timezone));
2416      $result = getdate($time);
2417      core_date::set_default_server_timezone();
2418  
2419      return $result;
2420  }
2421  
2422  /**
2423   * Given a GMT timestamp (seconds since epoch), offsets it by
2424   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2425   *
2426   * NOTE: this function does not include DST properly,
2427   *       you should use the PHP date stuff instead!
2428   *
2429   * @package core
2430   * @category time
2431   * @param int $date Timestamp in GMT
2432   * @param float|int|string $timezone user timezone
2433   * @return int
2434   */
2435  function usertime($date, $timezone=99) {
2436      $userdate = new DateTime('@' . $date);
2437      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2438      $dst = dst_offset_on($date, $timezone);
2439  
2440      return $date - $userdate->getOffset() + $dst;
2441  }
2442  
2443  /**
2444   * Get a formatted string representation of an interval between two unix timestamps.
2445   *
2446   * E.g.
2447   * $intervalstring = get_time_interval_string(12345600, 12345660);
2448   * Will produce the string:
2449   * '0d 0h 1m'
2450   *
2451   * @param int $time1 unix timestamp
2452   * @param int $time2 unix timestamp
2453   * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2454   * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
2455   *                         e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
2456   * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
2457   *                         e.g. instead of showing "3d", "3 days" will be returned.
2458   * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2459   */
2460  function get_time_interval_string(int $time1, int $time2, string $format = '',
2461          bool $dropzeroes = false, bool $fullformat = false): string {
2462      $dtdate = new DateTime();
2463      $dtdate->setTimeStamp($time1);
2464      $dtdate2 = new DateTime();
2465      $dtdate2->setTimeStamp($time2);
2466      $interval = $dtdate2->diff($dtdate);
2467  
2468      if (empty(trim($format))) {
2469          // Default to this key.
2470          $formatkey = 'dateintervaldayhrmin';
2471  
2472          if ($dropzeroes) {
2473              $units = [
2474                  'y' => 'yr',
2475                  'm' => 'mo',
2476                  'd' => 'day',
2477                  'h' => 'hr',
2478                  'i' => 'min',
2479                  's' => 'sec',
2480              ];
2481              $formatunits = [];
2482              foreach ($units as $key => $unit) {
2483                  if (empty($interval->$key)) {
2484                      continue;
2485                  }
2486                  $formatunits[] = $unit;
2487              }
2488              if (!empty($formatunits)) {
2489                  $formatkey = 'dateinterval' . implode("", $formatunits);
2490              }
2491          }
2492  
2493          if ($fullformat) {
2494              $formatkey .= 'full';
2495          }
2496          $format = get_string($formatkey, 'langconfig');
2497      }
2498      return $interval->format($format);
2499  }
2500  
2501  /**
2502   * Given a time, return the GMT timestamp of the most recent midnight
2503   * for the current user.
2504   *
2505   * @package core
2506   * @category time
2507   * @param int $date Timestamp in GMT
2508   * @param float|int|string $timezone user timezone
2509   * @return int Returns a GMT timestamp
2510   */
2511  function usergetmidnight($date, $timezone=99) {
2512  
2513      $userdate = usergetdate($date, $timezone);
2514  
2515      // Time of midnight of this user's day, in GMT.
2516      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2517  
2518  }
2519  
2520  /**
2521   * Returns a string that prints the user's timezone
2522   *
2523   * @package core
2524   * @category time
2525   * @param float|int|string $timezone user timezone
2526   * @return string
2527   */
2528  function usertimezone($timezone=99) {
2529      $tz = core_date::get_user_timezone($timezone);
2530      return core_date::get_localised_timezone($tz);
2531  }
2532  
2533  /**
2534   * Returns a float or a string which denotes the user's timezone
2535   * 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)
2536   * means that for this timezone there are also DST rules to be taken into account
2537   * Checks various settings and picks the most dominant of those which have a value
2538   *
2539   * @package core
2540   * @category time
2541   * @param float|int|string $tz timezone to calculate GMT time offset before
2542   *        calculating user timezone, 99 is default user timezone
2543   *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2544   * @return float|string
2545   */
2546  function get_user_timezone($tz = 99) {
2547      global $USER, $CFG;
2548  
2549      $timezones = array(
2550          $tz,
2551          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2552          isset($USER->timezone) ? $USER->timezone : 99,
2553          isset($CFG->timezone) ? $CFG->timezone : 99,
2554          );
2555  
2556      $tz = 99;
2557  
2558      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2559      foreach ($timezones as $nextvalue) {
2560          if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2561              $tz = $nextvalue;
2562          }
2563      }
2564      return is_numeric($tz) ? (float) $tz : $tz;
2565  }
2566  
2567  /**
2568   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2569   * - Note: Daylight saving only works for string timezones and not for float.
2570   *
2571   * @package core
2572   * @category time
2573   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2574   * @param int|float|string $strtimezone user timezone
2575   * @return int
2576   */
2577  function dst_offset_on($time, $strtimezone = null) {
2578      $tz = core_date::get_user_timezone($strtimezone);
2579      $date = new DateTime('@' . $time);
2580      $date->setTimezone(new DateTimeZone($tz));
2581      if ($date->format('I') == '1') {
2582          if ($tz === 'Australia/Lord_Howe') {
2583              return 1800;
2584          }
2585          return 3600;
2586      }
2587      return 0;
2588  }
2589  
2590  /**
2591   * Calculates when the day appears in specific month
2592   *
2593   * @package core
2594   * @category time
2595   * @param int $startday starting day of the month
2596   * @param int $weekday The day when week starts (normally taken from user preferences)
2597   * @param int $month The month whose day is sought
2598   * @param int $year The year of the month whose day is sought
2599   * @return int
2600   */
2601  function find_day_in_month($startday, $weekday, $month, $year) {
2602      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2603  
2604      $daysinmonth = days_in_month($month, $year);
2605      $daysinweek = count($calendartype->get_weekdays());
2606  
2607      if ($weekday == -1) {
2608          // Don't care about weekday, so return:
2609          //    abs($startday) if $startday != -1
2610          //    $daysinmonth otherwise.
2611          return ($startday == -1) ? $daysinmonth : abs($startday);
2612      }
2613  
2614      // From now on we 're looking for a specific weekday.
2615      // Give "end of month" its actual value, since we know it.
2616      if ($startday == -1) {
2617          $startday = -1 * $daysinmonth;
2618      }
2619  
2620      // Starting from day $startday, the sign is the direction.
2621      if ($startday < 1) {
2622          $startday = abs($startday);
2623          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2624  
2625          // This is the last such weekday of the month.
2626          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2627          if ($lastinmonth > $daysinmonth) {
2628              $lastinmonth -= $daysinweek;
2629          }
2630  
2631          // Find the first such weekday <= $startday.
2632          while ($lastinmonth > $startday) {
2633              $lastinmonth -= $daysinweek;
2634          }
2635  
2636          return $lastinmonth;
2637      } else {
2638          $indexweekday = dayofweek($startday, $month, $year);
2639  
2640          $diff = $weekday - $indexweekday;
2641          if ($diff < 0) {
2642              $diff += $daysinweek;
2643          }
2644  
2645          // This is the first such weekday of the month equal to or after $startday.
2646          $firstfromindex = $startday + $diff;
2647  
2648          return $firstfromindex;
2649      }
2650  }
2651  
2652  /**
2653   * Calculate the number of days in a given month
2654   *
2655   * @package core
2656   * @category time
2657   * @param int $month The month whose day count is sought
2658   * @param int $year The year of the month whose day count is sought
2659   * @return int
2660   */
2661  function days_in_month($month, $year) {
2662      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2663      return $calendartype->get_num_days_in_month($year, $month);
2664  }
2665  
2666  /**
2667   * Calculate the position in the week of a specific calendar day
2668   *
2669   * @package core
2670   * @category time
2671   * @param int $day The day of the date whose position in the week is sought
2672   * @param int $month The month of the date whose position in the week is sought
2673   * @param int $year The year of the date whose position in the week is sought
2674   * @return int
2675   */
2676  function dayofweek($day, $month, $year) {
2677      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2678      return $calendartype->get_weekday($year, $month, $day);
2679  }
2680  
2681  // USER AUTHENTICATION AND LOGIN.
2682  
2683  /**
2684   * Returns full login url.
2685   *
2686   * Any form submissions for authentication to this URL must include username,
2687   * password as well as a logintoken generated by \core\session\manager::get_login_token().
2688   *
2689   * @return string login url
2690   */
2691  function get_login_url() {
2692      global $CFG;
2693  
2694      return "$CFG->wwwroot/login/index.php";
2695  }
2696  
2697  /**
2698   * This function checks that the current user is logged in and has the
2699   * required privileges
2700   *
2701   * This function checks that the current user is logged in, and optionally
2702   * whether they are allowed to be in a particular course and view a particular
2703   * course module.
2704   * If they are not logged in, then it redirects them to the site login unless
2705   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2706   * case they are automatically logged in as guests.
2707   * If $courseid is given and the user is not enrolled in that course then the
2708   * user is redirected to the course enrolment page.
2709   * If $cm is given and the course module is hidden and the user is not a teacher
2710   * in the course then the user is redirected to the course home page.
2711   *
2712   * When $cm parameter specified, this function sets page layout to 'module'.
2713   * You need to change it manually later if some other layout needed.
2714   *
2715   * @package    core_access
2716   * @category   access
2717   *
2718   * @param mixed $courseorid id of the course or course object
2719   * @param bool $autologinguest default true
2720   * @param object $cm course module object
2721   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2722   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2723   *             in order to keep redirects working properly. MDL-14495
2724   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2725   * @return mixed Void, exit, and die depending on path
2726   * @throws coding_exception
2727   * @throws require_login_exception
2728   * @throws moodle_exception
2729   */
2730  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2731      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2732  
2733      // Must not redirect when byteserving already started.
2734      if (!empty($_SERVER['HTTP_RANGE'])) {
2735          $preventredirect = true;
2736      }
2737  
2738      if (AJAX_SCRIPT) {
2739          // We cannot redirect for AJAX scripts either.
2740          $preventredirect = true;
2741      }
2742  
2743      // Setup global $COURSE, themes, language and locale.
2744      if (!empty($courseorid)) {
2745          if (is_object($courseorid)) {
2746              $course = $courseorid;
2747          } else if ($courseorid == SITEID) {
2748              $course = clone($SITE);
2749          } else {
2750              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2751          }
2752          if ($cm) {
2753              if ($cm->course != $course->id) {
2754                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2755              }
2756              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2757              if (!($cm instanceof cm_info)) {
2758                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2759                  // db queries so this is not really a performance concern, however it is obviously
2760                  // better if you use get_fast_modinfo to get the cm before calling this.
2761                  $modinfo = get_fast_modinfo($course);
2762                  $cm = $modinfo->get_cm($cm->id);
2763              }
2764          }
2765      } else {
2766          // Do not touch global $COURSE via $PAGE->set_course(),
2767          // the reasons is we need to be able to call require_login() at any time!!
2768          $course = $SITE;
2769          if ($cm) {
2770              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2771          }
2772      }
2773  
2774      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2775      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2776      // risk leading the user back to the AJAX request URL.
2777      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2778          $setwantsurltome = false;
2779      }
2780  
2781      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2782      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2783          if ($preventredirect) {
2784              throw new require_login_session_timeout_exception();
2785          } else {
2786              if ($setwantsurltome) {
2787                  $SESSION->wantsurl = qualified_me();
2788              }
2789              redirect(get_login_url());
2790          }
2791      }
2792  
2793      // If the user is not even logged in yet then make sure they are.
2794      if (!isloggedin()) {
2795          if ($autologinguest && !empty($CFG->autologinguests)) {
2796              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2797                  // Misconfigured site guest, just redirect to login page.
2798                  redirect(get_login_url());
2799                  exit; // Never reached.
2800              }
2801              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2802              complete_user_login($guest);
2803              $USER->autologinguest = true;
2804              $SESSION->lang = $lang;
2805          } else {
2806              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2807              if ($preventredirect) {
2808                  throw new require_login_exception('You are not logged in');
2809              }
2810  
2811              if ($setwantsurltome) {
2812                  $SESSION->wantsurl = qualified_me();
2813              }
2814  
2815              // Give auth plugins an opportunity to authenticate or redirect to an external login page
2816              $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2817              foreach($authsequence as $authname) {
2818                  $authplugin = get_auth_plugin($authname);
2819                  $authplugin->pre_loginpage_hook();
2820                  if (isloggedin()) {
2821                      if ($cm) {
2822                          $modinfo = get_fast_modinfo($course);
2823                          $cm = $modinfo->get_cm($cm->id);
2824                      }
2825                      set_access_log_user();
2826                      break;
2827                  }
2828              }
2829  
2830              // If we're still not logged in then go to the login page
2831              if (!isloggedin()) {
2832                  redirect(get_login_url());
2833                  exit; // Never reached.
2834              }
2835          }
2836      }
2837  
2838      // Loginas as redirection if needed.
2839      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2840          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2841              if ($USER->loginascontext->instanceid != $course->id) {
2842                  throw new \moodle_exception('loginasonecourse', '',
2843                      $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2844              }
2845          }
2846      }
2847  
2848      // Check whether the user should be changing password (but only if it is REALLY them).
2849      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2850          $userauth = get_auth_plugin($USER->auth);
2851          if ($userauth->can_change_password() and !$preventredirect) {
2852              if ($setwantsurltome) {
2853                  $SESSION->wantsurl = qualified_me();
2854              }
2855              if ($changeurl = $userauth->change_password_url()) {
2856                  // Use plugin custom url.
2857                  redirect($changeurl);
2858              } else {
2859                  // Use moodle internal method.
2860                  redirect($CFG->wwwroot .'/login/change_password.php');
2861              }
2862          } else if ($userauth->can_change_password()) {
2863              throw new moodle_exception('forcepasswordchangenotice');
2864          } else {
2865              throw new moodle_exception('nopasswordchangeforced', 'auth');
2866          }
2867      }
2868  
2869      // Check that the user account is properly set up. If we can't redirect to
2870      // edit their profile and this is not a WS request, perform just the lax check.
2871      // It will allow them to use filepicker on the profile edit page.
2872  
2873      if ($preventredirect && !WS_SERVER) {
2874          $usernotfullysetup = user_not_fully_set_up($USER, false);
2875      } else {
2876          $usernotfullysetup = user_not_fully_set_up($USER, true);
2877      }
2878  
2879      if ($usernotfullysetup) {
2880          if ($preventredirect) {
2881              throw new moodle_exception('usernotfullysetup');
2882          }
2883          if ($setwantsurltome) {
2884              $SESSION->wantsurl = qualified_me();
2885          }
2886          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2887      }
2888  
2889      // Make sure the USER has a sesskey set up. Used for CSRF protection.
2890      sesskey();
2891  
2892      if (\core\session\manager::is_loggedinas()) {
2893          // During a "logged in as" session we should force all content to be cleaned because the
2894          // logged in user will be viewing potentially malicious user generated content.
2895          // See MDL-63786 for more details.
2896          $CFG->forceclean = true;
2897      }
2898  
2899      $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2900  
2901      // Do not bother admins with any formalities, except for activities pending deletion.
2902      if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2903          // Set the global $COURSE.
2904          if ($cm) {
2905              $PAGE->set_cm($cm, $course);
2906              $PAGE->set_pagelayout('incourse');
2907          } else if (!empty($courseorid)) {
2908              $PAGE->set_course($course);
2909          }
2910          // Set accesstime or the user will appear offline which messes up messaging.
2911          // Do not update access time for webservice or ajax requests.
2912          if (!WS_SERVER && !AJAX_SCRIPT) {
2913              user_accesstime_log($course->id);
2914          }
2915  
2916          foreach ($afterlogins as $plugintype => $plugins) {
2917              foreach ($plugins as $pluginfunction) {
2918                  $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2919              }
2920          }
2921          return;
2922      }
2923  
2924      // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2925      // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2926      if (!defined('NO_SITEPOLICY_CHECK')) {
2927          define('NO_SITEPOLICY_CHECK', false);
2928      }
2929  
2930      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2931      // Do not test if the script explicitly asked for skipping the site policies check.
2932      // Or if the user auth type is webservice.
2933      if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2934          $manager = new \core_privacy\local\sitepolicy\manager();
2935          if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2936              if ($preventredirect) {
2937                  throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2938              }
2939              if ($setwantsurltome) {
2940                  $SESSION->wantsurl = qualified_me();
2941              }
2942              redirect($policyurl);
2943          }
2944      }
2945  
2946      // Fetch the system context, the course context, and prefetch its child contexts.
2947      $sysctx = context_system::instance();
2948      $coursecontext = context_course::instance($course->id, MUST_EXIST);
2949      if ($cm) {
2950          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2951      } else {
2952          $cmcontext = null;
2953      }
2954  
2955      // If the site is currently under maintenance, then print a message.
2956      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2957          if ($preventredirect) {
2958              throw new require_login_exception('Maintenance in progress');
2959          }
2960          $PAGE->set_context(null);
2961          print_maintenance_message();
2962      }
2963  
2964      // Make sure the course itself is not hidden.
2965      if ($course->id == SITEID) {
2966          // Frontpage can not be hidden.
2967      } else {
2968          if (is_role_switched($course->id)) {
2969              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2970          } else {
2971              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2972                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2973                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2974                  if ($preventredirect) {
2975                      throw new require_login_exception('Course is hidden');
2976                  }
2977                  $PAGE->set_context(null);
2978                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
2979                  // the navigation will mess up when trying to find it.
2980                  navigation_node::override_active_url(new moodle_url('/'));
2981                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2982              }
2983          }
2984      }
2985  
2986      // Is the user enrolled?
2987      if ($course->id == SITEID) {
2988          // Everybody is enrolled on the frontpage.
2989      } else {
2990          if (\core\session\manager::is_loggedinas()) {
2991              // Make sure the REAL person can access this course first.
2992              $realuser = \core\session\manager::get_realuser();
2993              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2994                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2995                  if ($preventredirect) {
2996                      throw new require_login_exception('Invalid course login-as access');
2997                  }
2998                  $PAGE->set_context(null);
2999                  echo $OUTPUT->header();
3000                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
3001              }
3002          }
3003  
3004          $access = false;
3005  
3006          if (is_role_switched($course->id)) {
3007              // Ok, user had to be inside this course before the switch.
3008              $access = true;
3009  
3010          } else if (is_viewing($coursecontext, $USER)) {
3011              // Ok, no need to mess with enrol.
3012              $access = true;
3013  
3014          } else {
3015              if (isset($USER->enrol['enrolled'][$course->id])) {
3016                  if ($USER->enrol['enrolled'][$course->id] > time()) {
3017                      $access = true;
3018                      if (isset($USER->enrol['tempguest'][$course->id])) {
3019                          unset($USER->enrol['tempguest'][$course->id]);
3020                          remove_temp_course_roles($coursecontext);
3021                      }
3022                  } else {
3023                      // Expired.
3024                      unset($USER->enrol['enrolled'][$course->id]);
3025                  }
3026              }
3027              if (isset($USER->enrol['tempguest'][$course->id])) {
3028                  if ($USER->enrol['tempguest'][$course->id] == 0) {
3029                      $access = true;
3030                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
3031                      $access = true;
3032                  } else {
3033                      // Expired.
3034                      unset($USER->enrol['tempguest'][$course->id]);
3035                      remove_temp_course_roles($coursecontext);
3036                  }
3037              }
3038  
3039              if (!$access) {
3040                  // Cache not ok.
3041                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
3042                  if ($until !== false) {
3043                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
3044                      if ($until == 0) {
3045                          $until = ENROL_MAX_TIMESTAMP;
3046                      }
3047                      $USER->enrol['enrolled'][$course->id] = $until;
3048                      $access = true;
3049  
3050                  } else if (core_course_category::can_view_course_info($course)) {
3051                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
3052                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3053                      $enrols = enrol_get_plugins(true);
3054                      // First ask all enabled enrol instances in course if they want to auto enrol user.
3055                      foreach ($instances as $instance) {
3056                          if (!isset($enrols[$instance->enrol])) {
3057                              continue;
3058                          }
3059                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3060                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3061                          if ($until !== false) {
3062                              if ($until == 0) {
3063                                  $until = ENROL_MAX_TIMESTAMP;
3064                              }
3065                              $USER->enrol['enrolled'][$course->id] = $until;
3066                              $access = true;
3067                              break;
3068                          }
3069                      }
3070                      // If not enrolled yet try to gain temporary guest access.
3071                      if (!$access) {
3072                          foreach ($instances as $instance) {
3073                              if (!isset($enrols[$instance->enrol])) {
3074                                  continue;
3075                              }
3076                              // Get a duration for the guest access, a timestamp in the future or false.
3077                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3078                              if ($until !== false and $until > time()) {
3079                                  $USER->enrol['tempguest'][$course->id] = $until;
3080                                  $access = true;
3081                                  break;
3082                              }
3083                          }
3084                      }
3085                  } else {
3086                      // User is not enrolled and is not allowed to browse courses here.
3087                      if ($preventredirect) {
3088                          throw new require_login_exception('Course is not available');
3089                      }
3090                      $PAGE->set_context(null);
3091                      // We need to override the navigation URL as the course won't have been added to the navigation and thus
3092                      // the navigation will mess up when trying to find it.
3093                      navigation_node::override_active_url(new moodle_url('/'));
3094                      notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3095                  }
3096              }
3097          }
3098  
3099          if (!$access) {
3100              if ($preventredirect) {
3101                  throw new require_login_exception('Not enrolled');
3102              }
3103              if ($setwantsurltome) {
3104                  $SESSION->wantsurl = qualified_me();
3105              }
3106              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3107          }
3108      }
3109  
3110      // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3111      if ($cm && $cm->deletioninprogress) {
3112          if ($preventredirect) {
3113              throw new moodle_exception('activityisscheduledfordeletion');
3114          }
3115          require_once($CFG->dirroot . '/course/lib.php');
3116          redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3117      }
3118  
3119      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3120      if ($cm && !$cm->uservisible) {
3121          if ($preventredirect) {
3122              throw new require_login_exception('Activity is hidden');
3123          }
3124          // Get the error message that activity is not available and why (if explanation can be shown to the user).
3125          $PAGE->set_course($course);
3126          $renderer = $PAGE->get_renderer('course');
3127          $message = $renderer->course_section_cm_unavailable_error_message($cm);
3128          redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3129      }
3130  
3131      // Set the global $COURSE.
3132      if ($cm) {
3133          $PAGE->set_cm($cm, $course);
3134          $PAGE->set_pagelayout('incourse');
3135      } else if (!empty($courseorid)) {
3136          $PAGE->set_course($course);
3137      }
3138  
3139      foreach ($afterlogins as $plugintype => $plugins) {
3140          foreach ($plugins as $pluginfunction) {
3141              $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3142          }
3143      }
3144  
3145      // Finally access granted, update lastaccess times.
3146      // Do not update access time for webservice or ajax requests.
3147      if (!WS_SERVER && !AJAX_SCRIPT) {
3148          user_accesstime_log($course->id);
3149      }
3150  }
3151  
3152  /**
3153   * A convenience function for where we must be logged in as admin
3154   * @return void
3155   */
3156  function require_admin() {
3157      require_login(null, false);
3158      require_capability('moodle/site:config', context_system::instance());
3159  }
3160  
3161  /**
3162   * This function just makes sure a user is logged out.
3163   *
3164   * @package    core_access
3165   * @category   access
3166   */
3167  function require_logout() {
3168      global $USER, $DB;
3169  
3170      if (!isloggedin()) {
3171          // This should not happen often, no need for hooks or events here.
3172          \core\session\manager::terminate_current();
3173          return;
3174      }
3175  
3176      // Execute hooks before action.
3177      $authplugins = array();
3178      $authsequence = get_enabled_auth_plugins();
3179      foreach ($authsequence as $authname) {
3180          $authplugins[$authname] = get_auth_plugin($authname);
3181          $authplugins[$authname]->prelogout_hook();
3182      }
3183  
3184      // Store info that gets removed during logout.
3185      $sid = session_id();
3186      $event = \core\event\user_loggedout::create(
3187          array(
3188              'userid' => $USER->id,
3189              'objectid' => $USER->id,
3190              'other' => array('sessionid' => $sid),
3191          )
3192      );
3193      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3194          $event->add_record_snapshot('sessions', $session);
3195      }
3196  
3197      // Clone of $USER object to be used by auth plugins.
3198      $user = fullclone($USER);
3199  
3200      // Delete session record and drop $_SESSION content.
3201      \core\session\manager::terminate_current();
3202  
3203      // Trigger event AFTER action.
3204      $event->trigger();
3205  
3206      // Hook to execute auth plugins redirection after event trigger.
3207      foreach ($authplugins as $authplugin) {
3208          $authplugin->postlogout_hook($user);
3209      }
3210  }
3211  
3212  /**
3213   * Weaker version of require_login()
3214   *
3215   * This is a weaker version of {@link require_login()} which only requires login
3216   * when called from within a course rather than the site page, unless
3217   * the forcelogin option is turned on.
3218   * @see require_login()
3219   *
3220   * @package    core_access
3221   * @category   access
3222   *
3223   * @param mixed $courseorid The course object or id in question
3224   * @param bool $autologinguest Allow autologin guests if that is wanted
3225   * @param object $cm Course activity module if known
3226   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3227   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3228   *             in order to keep redirects working properly. MDL-14495
3229   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3230   * @return void
3231   * @throws coding_exception
3232   */
3233  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3234      global $CFG, $PAGE, $SITE;
3235      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3236            or (!is_object($courseorid) and $courseorid == SITEID));
3237      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3238          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3239          // db queries so this is not really a performance concern, however it is obviously
3240          // better if you use get_fast_modinfo to get the cm before calling this.
3241          if (is_object($courseorid)) {
3242              $course = $courseorid;
3243          } else {
3244              $course = clone($SITE);
3245          }
3246          $modinfo = get_fast_modinfo($course);
3247          $cm = $modinfo->get_cm($cm->id);
3248      }
3249      if (!empty($CFG->forcelogin)) {
3250          // Login required for both SITE and courses.
3251          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3252  
3253      } else if ($issite && !empty($cm) and !$cm->uservisible) {
3254          // Always login for hidden activities.
3255          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3256  
3257      } else if (isloggedin() && !isguestuser()) {
3258          // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3259          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3260  
3261      } else if ($issite) {
3262          // Login for SITE not required.
3263          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3264          if (!empty($courseorid)) {
3265              if (is_object($courseorid)) {
3266                  $course = $courseorid;
3267              } else {
3268                  $course = clone $SITE;
3269              }
3270              if ($cm) {
3271                  if ($cm->course != $course->id) {
3272                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3273                  }
3274                  $PAGE->set_cm($cm, $course);
3275                  $PAGE->set_pagelayout('incourse');
3276              } else {
3277                  $PAGE->set_course($course);
3278              }
3279          } else {
3280              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3281              $PAGE->set_course($PAGE->course);
3282          }
3283          // Do not update access time for webservice or ajax requests.
3284          if (!WS_SERVER && !AJAX_SCRIPT) {
3285              user_accesstime_log(SITEID);
3286          }
3287          return;
3288  
3289      } else {
3290          // Course login always required.
3291          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3292      }
3293  }
3294  
3295  /**
3296   * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3297   *
3298   * @param  string $keyvalue the key value
3299   * @param  string $script   unique script identifier
3300   * @param  int $instance    instance id
3301   * @return stdClass the key entry in the user_private_key table
3302   * @since Moodle 3.2
3303   * @throws moodle_exception
3304   */
3305  function validate_user_key($keyvalue, $script, $instance) {
3306      global $DB;
3307  
3308      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3309          throw new \moodle_exception('invalidkey');
3310      }
3311  
3312      if (!empty($key->validuntil) and $key->validuntil < time()) {
3313          throw new \moodle_exception('expiredkey');
3314      }
3315  
3316      if ($key->iprestriction) {
3317          $remoteaddr = getremoteaddr(null);
3318          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3319              throw new \moodle_exception('ipmismatch');
3320          }
3321      }
3322      return $key;
3323  }
3324  
3325  /**
3326   * Require key login. Function terminates with error if key not found or incorrect.
3327   *
3328   * @uses NO_MOODLE_COOKIES
3329   * @uses PARAM_ALPHANUM
3330   * @param string $script unique script identifier
3331   * @param int $instance optional instance id
3332   * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3333   * @return int Instance ID
3334   */
3335  function require_user_key_login($script, $instance = null, $keyvalue = null) {
3336      global $DB;
3337  
3338      if (!NO_MOODLE_COOKIES) {
3339          throw new \moodle_exception('sessioncookiesdisable');
3340      }
3341  
3342      // Extra safety.
3343      \core\session\manager::write_close();
3344  
3345      if (null === $keyvalue) {
3346          $keyvalue = required_param('key', PARAM_ALPHANUM);
3347      }
3348  
3349      $key = validate_user_key($keyvalue, $script, $instance);
3350  
3351      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3352          throw new \moodle_exception('invaliduserid');
3353      }
3354  
3355      core_user::require_active_user($user, true, true);
3356  
3357      // Emulate normal session.
3358      enrol_check_plugins($user, false);
3359      \core\session\manager::set_user($user);
3360  
3361      // Note we are not using normal login.
3362      if (!defined('USER_KEY_LOGIN')) {
3363          define('USER_KEY_LOGIN', true);
3364      }
3365  
3366      // Return instance id - it might be empty.
3367      return $key->instance;
3368  }
3369  
3370  /**
3371   * Creates a new private user access key.
3372   *
3373   * @param string $script unique target identifier
3374   * @param int $userid
3375   * @param int $instance optional instance id
3376   * @param string $iprestriction optional ip restricted access
3377   * @param int $validuntil key valid only until given data
3378   * @return string access key value
3379   */
3380  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3381      global $DB;
3382  
3383      $key = new stdClass();
3384      $key->script        = $script;
3385      $key->userid        = $userid;
3386      $key->instance      = $instance;
3387      $key->iprestriction = $iprestriction;
3388      $key->validuntil    = $validuntil;
3389      $key->timecreated   = time();
3390  
3391      // Something long and unique.
3392      $key->value         = md5($userid.'_'.time().random_string(40));
3393      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3394          // Must be unique.
3395          $key->value     = md5($userid.'_'.time().random_string(40));
3396      }
3397      $DB->insert_record('user_private_key', $key);
3398      return $key->value;
3399  }
3400  
3401  /**
3402   * Delete the user's new private user access keys for a particular script.
3403   *
3404   * @param string $script unique target identifier
3405   * @param int $userid
3406   * @return void
3407   */
3408  function delete_user_key($script, $userid) {
3409      global $DB;
3410      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3411  }
3412  
3413  /**
3414   * Gets a private user access key (and creates one if one doesn't exist).
3415   *
3416   * @param string $script unique target identifier
3417   * @param int $userid
3418   * @param int $instance optional instance id
3419   * @param string $iprestriction optional ip restricted access
3420   * @param int $validuntil key valid only until given date
3421   * @return string access key value
3422   */
3423  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3424      global $DB;
3425  
3426      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3427                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
3428                                                           'validuntil' => $validuntil))) {
3429          return $key->value;
3430      } else {
3431          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3432      }
3433  }
3434  
3435  
3436  /**
3437   * Modify the user table by setting the currently logged in user's last login to now.
3438   *
3439   * @return bool Always returns true
3440   */
3441  function update_user_login_times() {
3442      global $USER, $DB, $SESSION;
3443  
3444      if (isguestuser()) {
3445          // Do not update guest access times/ips for performance.
3446          return true;
3447      }
3448  
3449      if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
3450          // Do not update user login time when using user key login.
3451          return true;
3452      }
3453  
3454      $now = time();
3455  
3456      $user = new stdClass();
3457      $user->id = $USER->id;
3458  
3459      // Make sure all users that logged in have some firstaccess.
3460      if ($USER->firstaccess == 0) {
3461          $USER->firstaccess = $user->firstaccess = $now;
3462      }
3463  
3464      // Store the previous current as lastlogin.
3465      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3466  
3467      $USER->currentlogin = $user->currentlogin = $now;
3468  
3469      // Function user_accesstime_log() may not update immediately, better do it here.
3470      $USER->lastaccess = $user->lastaccess = $now;
3471      $SESSION->userpreviousip = $USER->lastip;
3472      $USER->lastip = $user->lastip = getremoteaddr();
3473  
3474      // Note: do not call user_update_user() here because this is part of the login process,
3475      //       the login event means that these fields were updated.
3476      $DB->update_record('user', $user);
3477      return true;
3478  }
3479  
3480  /**
3481   * Determines if a user has completed setting up their account.
3482   *
3483   * The lax mode (with $strict = false) has been introduced for special cases
3484   * only where we want to skip certain checks intentionally. This is valid in
3485   * certain mnet or ajax scenarios when the user cannot / should not be
3486   * redirected to edit their profile. In most cases, you should perform the
3487   * strict check.
3488   *
3489   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3490   * @param bool $strict Be more strict and assert id and custom profile fields set, too
3491   * @return bool
3492   */
3493  function user_not_fully_set_up($user, $strict = true) {
3494      global $CFG, $SESSION, $USER;
3495      require_once($CFG->dirroot.'/user/profile/lib.php');
3496  
3497      // If the user is setup then store this in the session to avoid re-checking.
3498      // Some edge cases are when the users email starts to bounce or the
3499      // configuration for custom fields has changed while they are logged in so
3500      // we re-check this fully every hour for the rare cases it has changed.
3501      if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
3502           isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
3503          return false;
3504      }
3505  
3506      if (isguestuser($user)) {
3507          return false;
3508      }
3509  
3510      if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3511          return true;
3512      }
3513  
3514      if ($strict) {
3515          if (empty($user->id)) {
3516              // Strict mode can be used with existing accounts only.
3517              return true;
3518          }
3519          if (!profile_has_required_custom_fields_set($user->id)) {
3520              return true;
3521          }
3522          if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3523              $SESSION->fullysetupstrict = time();
3524          }
3525      }
3526  
3527      return false;
3528  }
3529  
3530  /**
3531   * Check whether the user has exceeded the bounce threshold
3532   *
3533   * @param stdClass $user A {@link $USER} object
3534   * @return bool true => User has exceeded bounce threshold
3535   */
3536  function over_bounce_threshold($user) {
3537      global $CFG, $DB;
3538  
3539      if (empty($CFG->handlebounces)) {
3540          return false;
3541      }
3542  
3543      if (empty($user->id)) {
3544          // No real (DB) user, nothing to do here.
3545          return false;
3546      }
3547  
3548      // Set sensible defaults.
3549      if (empty($CFG->minbounces)) {
3550          $CFG->minbounces = 10;
3551      }
3552      if (empty($CFG->bounceratio)) {
3553          $CFG->bounceratio = .20;
3554      }
3555      $bouncecount = 0;
3556      $sendcount = 0;
3557      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3558          $bouncecount = $bounce->value;
3559      }
3560      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3561          $sendcount = $send->value;
3562      }
3563      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3564  }
3565  
3566  /**
3567   * Used to increment or reset email sent count
3568   *
3569   * @param stdClass $user object containing an id
3570   * @param bool $reset will reset the count to 0
3571   * @return void
3572   */
3573  function set_send_count($user, $reset=false) {
3574      global $DB;
3575  
3576      if (empty($user->id)) {
3577          // No real (DB) user, nothing to do here.
3578          return;
3579      }
3580  
3581      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3582          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3583          $DB->update_record('user_preferences', $pref);
3584      } else if (!empty($reset)) {
3585          // If it's not there and we're resetting, don't bother. Make a new one.
3586          $pref = new stdClass();
3587          $pref->name   = 'email_send_count';
3588          $pref->value  = 1;
3589          $pref->userid = $user->id;
3590          $DB->insert_record('user_preferences', $pref, false);
3591      }
3592  }
3593  
3594  /**
3595   * Increment or reset user's email bounce count
3596   *
3597   * @param stdClass $user object containing an id
3598   * @param bool $reset will reset the count to 0
3599   */
3600  function set_bounce_count($user, $reset=false) {
3601      global $DB;
3602  
3603      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3604          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3605          $DB->update_record('user_preferences', $pref);
3606      } else if (!empty($reset)) {
3607          // If it's not there and we're resetting, don't bother. Make a new one.
3608          $pref = new stdClass();
3609          $pref->name   = 'email_bounce_count';
3610          $pref->value  = 1;
3611          $pref->userid = $user->id;
3612          $DB->insert_record('user_preferences', $pref, false);
3613      }
3614  }
3615  
3616  /**
3617   * Determines if the logged in user is currently moving an activity
3618   *
3619   * @param int $courseid The id of the course being tested
3620   * @return bool
3621   */
3622  function ismoving($courseid) {
3623      global $USER;
3624  
3625      if (!empty($USER->activitycopy)) {
3626          return ($USER->activitycopycourse == $courseid);
3627      }
3628      return false;
3629  }
3630  
3631  /**
3632   * Returns a persons full name
3633   *
3634   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3635   * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3636   * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3637   * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3638   *
3639   * @param stdClass $user A {@link $USER} object to get full name of.
3640   * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3641   * @return string
3642   */
3643  function fullname($user, $override=false) {
3644      // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3645      // Uses of it should be updated to use the new API and pass updated arguments.
3646  
3647      // Return an empty string if there is no user.
3648      if (empty($user)) {
3649          return '';
3650      }
3651  
3652      $options = ['override' => $override];
3653      return core_user::get_fullname($user, null, $options);
3654  }
3655  
3656  /**
3657   * Reduces lines of duplicated code for getting user name fields.
3658   *
3659   * See also {@link user_picture::unalias()}
3660   *
3661   * @param object $addtoobject Object to add user name fields to.
3662   * @param object $secondobject Object that contains user name field information.
3663   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3664   * @param array $additionalfields Additional fields to be matched with data in the second object.
3665   * The key can be set to the user table field name.
3666   * @return object User name fields.
3667   */
3668  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3669      $fields = [];
3670      foreach (\core_user\fields::get_name_fields() as $field) {
3671          $fields[$field] = $prefix . $field;
3672      }
3673      if ($additionalfields) {
3674          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3675          // the key is a number and then sets the key to the array value.
3676          foreach ($additionalfields as $key => $value) {
3677              if (is_numeric($key)) {
3678                  $additionalfields[$value] = $prefix . $value;
3679                  unset($additionalfields[$key]);
3680              } else {
3681                  $additionalfields[$key] = $prefix . $value;
3682              }
3683          }
3684          $fields = array_merge($fields, $additionalfields);
3685      }
3686      foreach ($fields as $key => $field) {
3687          // Important that we have all of the user name fields present in the object that we are sending back.
3688          $addtoobject->$key = '';
3689          if (isset($secondobject->$field)) {
3690              $addtoobject->$key = $secondobject->$field;
3691          }
3692      }
3693      return $addtoobject;
3694  }
3695  
3696  /**
3697   * Returns an array of values in order of occurance in a provided string.
3698   * The key in the result is the character postion in the string.
3699   *
3700   * @param array $values Values to be found in the string format
3701   * @param string $stringformat The string which may contain values being searched for.
3702   * @return array An array of values in order according to placement in the string format.
3703   */
3704  function order_in_string($values, $stringformat) {
3705      $valuearray = array();
3706      foreach ($values as $value) {
3707          $pattern = "/$value\b/";
3708          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3709          if (preg_match($pattern, $stringformat)) {
3710              $replacement = "thing";
3711              // Replace the value with something more unique to ensure we get the right position when using strpos().
3712              $newformat = preg_replace($pattern, $replacement, $stringformat);
3713              $position = strpos($newformat, $replacement);
3714              $valuearray[$position] = $value;
3715          }
3716      }
3717      ksort($valuearray);
3718      return $valuearray;
3719  }
3720  
3721  /**
3722   * Returns whether a given authentication plugin exists.
3723   *
3724   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3725   * @return boolean Whether the plugin is available.
3726   */
3727  function exists_auth_plugin($auth) {
3728      global $CFG;
3729  
3730      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3731          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3732      }
3733      return false;
3734  }
3735  
3736  /**
3737   * Checks if a given plugin is in the list of enabled authentication plugins.
3738   *
3739   * @param string $auth Authentication plugin.
3740   * @return boolean Whether the plugin is enabled.
3741   */
3742  function is_enabled_auth($auth) {
3743      if (empty($auth)) {
3744          return false;
3745      }
3746  
3747      $enabled = get_enabled_auth_plugins();
3748  
3749      return in_array($auth, $enabled);
3750  }
3751  
3752  /**
3753   * Returns an authentication plugin instance.
3754   *
3755   * @param string $auth name of authentication plugin
3756   * @return auth_plugin_base An instance of the required authentication plugin.
3757   */
3758  function get_auth_plugin($auth) {
3759      global $CFG;
3760  
3761      // Check the plugin exists first.
3762      if (! exists_auth_plugin($auth)) {
3763          throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3764      }
3765  
3766      // Return auth plugin instance.
3767      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3768      $class = "auth_plugin_$auth";
3769      return new $class;
3770  }
3771  
3772  /**
3773   * Returns array of active auth plugins.
3774   *
3775   * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3776   * @return array
3777   */
3778  function get_enabled_auth_plugins($fix=false) {
3779      global $CFG;
3780  
3781      $default = array('manual', 'nologin');
3782  
3783      if (empty($CFG->auth)) {
3784          $auths = array();
3785      } else {
3786          $auths = explode(',', $CFG->auth);
3787      }
3788  
3789      $auths = array_unique($auths);
3790      $oldauthconfig = implode(',', $auths);
3791      foreach ($auths as $k => $authname) {
3792          if (in_array($authname, $default)) {
3793              // The manual and nologin plugin never need to be stored.
3794              unset($auths[$k]);
3795          } else if (!exists_auth_plugin($authname)) {
3796              debugging(get_string('authpluginnotfound', 'debug', $authname));
3797              unset($auths[$k]);
3798          }
3799      }
3800  
3801      // Ideally only explicit interaction from a human admin should trigger a
3802      // change in auth config, see MDL-70424 for details.
3803      if ($fix) {
3804          $newconfig = implode(',', $auths);
3805          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3806              add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3807              set_config('auth', $newconfig);
3808          }
3809      }
3810  
3811      return (array_merge($default, $auths));
3812  }
3813  
3814  /**
3815   * Returns true if an internal authentication method is being used.
3816   * if method not specified then, global default is assumed
3817   *
3818   * @param string $auth Form of authentication required
3819   * @return bool
3820   */
3821  function is_internal_auth($auth) {
3822      // Throws error if bad $auth.
3823      $authplugin = get_auth_plugin($auth);
3824      return $authplugin->is_internal();
3825  }
3826  
3827  /**
3828   * Returns true if the user is a 'restored' one.
3829   *
3830   * Used in the login process to inform the user and allow him/her to reset the password
3831   *
3832   * @param string $username username to be checked
3833   * @return bool
3834   */
3835  function is_restored_user($username) {
3836      global $CFG, $DB;
3837  
3838      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3839  }
3840  
3841  /**
3842   * Returns an array of user fields
3843   *
3844   * @return array User field/column names
3845   */
3846  function get_user_fieldnames() {
3847      global $DB;
3848  
3849      $fieldarray = $DB->get_columns('user');
3850      unset($fieldarray['id']);
3851      $fieldarray = array_keys($fieldarray);
3852  
3853      return $fieldarray;
3854  }
3855  
3856  /**
3857   * Returns the string of the language for the new user.
3858   *
3859   * @return string language for the new user
3860   */
3861  function get_newuser_language() {
3862      global $CFG, $SESSION;
3863      return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3864  }
3865  
3866  /**
3867   * Creates a bare-bones user record
3868   *
3869   * @todo Outline auth types and provide code example
3870   *
3871   * @param string $username New user's username to add to record
3872   * @param string $password New user's password to add to record
3873   * @param string $auth Form of authentication required
3874   * @return stdClass A complete user object
3875   */
3876  function create_user_record($username, $password, $auth = 'manual') {
3877      global $CFG, $DB, $SESSION;
3878      require_once($CFG->dirroot.'/user/profile/lib.php');
3879      require_once($CFG->dirroot.'/user/lib.php');
3880  
3881      // Just in case check text case.
3882      $username = trim(core_text::strtolower($username));
3883  
3884      $authplugin = get_auth_plugin($auth);
3885      $customfields = $authplugin->get_custom_user_profile_fields();
3886      $newuser = new stdClass();
3887      if ($newinfo = $authplugin->get_userinfo($username)) {
3888          $newinfo = truncate_userinfo($newinfo);
3889          foreach ($newinfo as $key => $value) {
3890              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3891                  $newuser->$key = $value;
3892              }
3893          }
3894      }
3895  
3896      if (!empty($newuser->email)) {
3897          if (email_is_not_allowed($newuser->email)) {
3898              unset($newuser->email);
3899          }
3900      }
3901  
3902      $newuser->auth = $auth;
3903      $newuser->username = $username;
3904  
3905      // Fix for MDL-8480
3906      // user CFG lang for user if $newuser->lang is empty
3907      // or $user->lang is not an installed language.
3908      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3909          $newuser->lang = get_newuser_language();
3910      }
3911      $newuser->confirmed = 1;
3912      $newuser->lastip = getremoteaddr();
3913      $newuser->timecreated = time();
3914      $newuser->timemodified = $newuser->timecreated;
3915      $newuser->mnethostid = $CFG->mnet_localhost_id;
3916  
3917      $newuser->id = user_create_user($newuser, false, false);
3918  
3919      // Save user profile data.
3920      profile_save_data($newuser);
3921  
3922      $user = get_complete_user_data('id', $newuser->id);
3923      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3924          set_user_preference('auth_forcepasswordchange', 1, $user);
3925      }
3926      // Set the password.
3927      update_internal_user_password($user, $password);
3928  
3929      // Trigger event.
3930      \core\event\user_created::create_from_userid($newuser->id)->trigger();
3931  
3932      return $user;
3933  }
3934  
3935  /**
3936   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3937   *
3938   * @param string $username user's username to update the record
3939   * @return stdClass A complete user object
3940   */
3941  function update_user_record($username) {
3942      global $DB, $CFG;
3943      // Just in case check text case.
3944      $username = trim(core_text::strtolower($username));
3945  
3946      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3947      return update_user_record_by_id($oldinfo->id);
3948  }
3949  
3950  /**
3951   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3952   *
3953   * @param int $id user id
3954   * @return stdClass A complete user object
3955   */
3956  function update_user_record_by_id($id) {
3957      global $DB, $CFG;
3958      require_once($CFG->dirroot."/user/profile/lib.php");
3959      require_once($CFG->dirroot.'/user/lib.php');
3960  
3961      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3962      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3963  
3964      $newuser = array();
3965      $userauth = get_auth_plugin($oldinfo->auth);
3966  
3967      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3968          $newinfo = truncate_userinfo($newinfo);
3969          $customfields = $userauth->get_custom_user_profile_fields();
3970  
3971          foreach ($newinfo as $key => $value) {
3972              $iscustom = in_array($key, $customfields);
3973              if (!$iscustom) {
3974                  $key = strtolower($key);
3975              }
3976              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3977                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3978                  // Unknown or must not be changed.
3979                  continue;
3980              }
3981              if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3982                  continue;
3983              }
3984              $confval = $userauth->config->{'field_updatelocal_' . $key};
3985              $lockval = $userauth->config->{'field_lock_' . $key};
3986              if ($confval === 'onlogin') {
3987                  // MDL-4207 Don't overwrite modified user profile values with
3988                  // empty LDAP values when 'unlocked if empty' is set. The purpose
3989                  // of the setting 'unlocked if empty' is to allow the user to fill
3990                  // in a value for the selected field _if LDAP is giving
3991                  // nothing_ for this field. Thus it makes sense to let this value
3992                  // stand in until LDAP is giving a value for this field.
3993                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
3994                      if ($iscustom || (in_array($key, $userauth->userfields) &&
3995                              ((string)$oldinfo->$key !== (string)$value))) {
3996                          $newuser[$key] = (string)$value;
3997                      }
3998                  }
3999              }
4000          }
4001          if ($newuser) {
4002              $newuser['id'] = $oldinfo->id;
4003              $newuser['timemodified'] = time();
4004              user_update_user((object) $newuser, false, false);
4005  
4006              // Save user profile data.
4007              profile_save_data((object) $newuser);
4008  
4009              // Trigger event.
4010              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
4011          }
4012      }
4013  
4014      return get_complete_user_data('id', $oldinfo->id);
4015  }
4016  
4017  /**
4018   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4019   *
4020   * @param array $info Array of user properties to truncate if needed
4021   * @return array The now truncated information that was passed in
4022   */
4023  function truncate_userinfo(array $info) {
4024      // Define the limits.
4025      $limit = array(
4026          'username'    => 100,
4027          'idnumber'    => 255,
4028          'firstname'   => 100,
4029          'lastname'    => 100,
4030          'email'       => 100,
4031          'phone1'      =>  20,
4032          'phone2'      =>  20,
4033          'institution' => 255,
4034          'department'  => 255,
4035          'address'     => 255,
4036          'city'        => 120,
4037          'country'     =>   2,
4038      );
4039  
4040      // Apply where needed.
4041      foreach (array_keys($info) as $key) {
4042          if (!empty($limit[$key])) {
4043              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4044          }
4045      }
4046  
4047      return $info;
4048  }
4049  
4050  /**
4051   * Marks user deleted in internal user database and notifies the auth plugin.
4052   * Also unenrols user from all roles and does other cleanup.
4053   *
4054   * Any plugin that needs to purge user data should register the 'user_deleted' event.
4055   *
4056   * @param stdClass $user full user object before delete
4057   * @return boolean success
4058   * @throws coding_exception if invalid $user parameter detected
4059   */
4060  function delete_user(stdClass $user) {
4061      global $CFG, $DB, $SESSION;
4062      require_once($CFG->libdir.'/grouplib.php');
4063      require_once($CFG->libdir.'/gradelib.php');
4064      require_once($CFG->dirroot.'/message/lib.php');
4065      require_once($CFG->dirroot.'/user/lib.php');
4066  
4067      // Make sure nobody sends bogus record type as parameter.
4068      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4069          throw new coding_exception('Invalid $user parameter in delete_user() detected');
4070      }
4071  
4072      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4073      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4074          debugging('Attempt to delete unknown user account.');
4075          return false;
4076      }
4077  
4078      // There must be always exactly one guest record, originally the guest account was identified by username only,
4079      // now we use $CFG->siteguest for performance reasons.
4080      if ($user->username === 'guest' or isguestuser($user)) {
4081          debugging('Guest user account can not be deleted.');
4082          return false;
4083      }
4084  
4085      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4086      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4087      if ($user->auth === 'manual' and is_siteadmin($user)) {
4088          debugging('Local administrator accounts can not be deleted.');
4089          return false;
4090      }
4091  
4092      // Allow plugins to use this user object before we completely delete it.
4093      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4094          foreach ($pluginsfunction as $plugintype => $plugins) {
4095              foreach ($plugins as $pluginfunction) {
4096                  $pluginfunction($user);
4097              }
4098          }
4099      }
4100  
4101      // Keep user record before updating it, as we have to pass this to user_deleted event.
4102      $olduser = clone $user;
4103  
4104      // Keep a copy of user context, we need it for event.
4105      $usercontext = context_user::instance($user->id);
4106  
4107      // Remove user from communication rooms immediately.
4108      if (core_communication\api::is_available()) {
4109          foreach (enrol_get_users_courses($user->id) as $course) {
4110              $communication = \core_communication\processor::load_by_instance(
4111                  context: \core\context\course::instance($course->id),
4112                  component: 'core_course',
4113                  instancetype: 'coursecommunication',
4114                  instanceid: $course->id,
4115              );
4116              if ($communication !== null) {
4117                  $communication->get_room_user_provider()->remove_members_from_room([$user->id]);
4118                  $communication->delete_instance_user_mapping([$user->id]);
4119              }
4120          }
4121      }
4122  
4123      // Delete all grades - backup is kept in grade_grades_history table.
4124      grade_user_delete($user->id);
4125  
4126      // TODO: remove from cohorts using standard API here.
4127  
4128      // Remove user tags.
4129      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4130  
4131      // Unconditionally unenrol from all courses.
4132      enrol_user_delete($user);
4133  
4134      // Unenrol from all roles in all contexts.
4135      // This might be slow but it is really needed - modules might do some extra cleanup!
4136      role_unassign_all(array('userid' => $user->id));
4137  
4138      // Notify the competency subsystem.
4139      \core_competency\api::hook_user_deleted($user->id);
4140  
4141      // Now do a brute force cleanup.
4142  
4143      // Delete all user events and subscription events.
4144      $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4145  
4146      // Now, delete all calendar subscription from the user.
4147      $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4148  
4149      // Remove from all cohorts.
4150      $DB->delete_records('cohort_members', array('userid' => $user->id));
4151  
4152      // Remove from all groups.
4153      $DB->delete_records('groups_members', array('userid' => $user->id));
4154  
4155      // Brute force unenrol from all courses.
4156      $DB->delete_records('user_enrolments', array('userid' => $user->id));
4157  
4158      // Purge user preferences.
4159      $DB->delete_records('user_preferences', array('userid' => $user->id));
4160  
4161      // Purge user extra profile info.
4162      $DB->delete_records('user_info_data', array('userid' => $user->id));
4163  
4164      // Purge log of previous password hashes.
4165      $DB->delete_records('user_password_history', array('userid' => $user->id));
4166  
4167      // Last course access not necessary either.
4168      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4169      // Remove all user tokens.
4170      $DB->delete_records('external_tokens', array('userid' => $user->id));
4171  
4172      // Unauthorise the user for all services.
4173      $DB->delete_records('external_services_users', array('userid' => $user->id));
4174  
4175      // Remove users private keys.
4176      $DB->delete_records('user_private_key', array('userid' => $user->id));
4177  
4178      // Remove users customised pages.
4179      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4180  
4181      // Remove user's oauth2 refresh tokens, if present.
4182      $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
4183  
4184      // Delete user from $SESSION->bulk_users.
4185      if (isset($SESSION->bulk_users[$user->id])) {
4186          unset($SESSION->bulk_users[$user->id]);
4187      }
4188  
4189      // Force logout - may fail if file based sessions used, sorry.
4190      \core\session\manager::kill_user_sessions($user->id);
4191  
4192      // Generate username from email address, or a fake email.
4193      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4194  
4195      $deltime = time();
4196      $deltimelength = core_text::strlen((string) $deltime);
4197  
4198      // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4199      $delname = clean_param($delemail, PARAM_USERNAME);
4200      $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4201  
4202      // Workaround for bulk deletes of users with the same email address.
4203      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4204          $delname++;
4205      }
4206  
4207      // Mark internal user record as "deleted".
4208      $updateuser = new stdClass();
4209      $updateuser->id           = $user->id;
4210      $updateuser->deleted      = 1;
4211      $updateuser->username     = $delname;            // Remember it just in case.
4212      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4213      $updateuser->idnumber     = '';                  // Clear this field to free it up.
4214      $updateuser->picture      = 0;
4215      $updateuser->timemodified = $deltime;
4216  
4217      // Don't trigger update event, as user is being deleted.
4218      user_update_user($updateuser, false, false);
4219  
4220      // Delete all content associated with the user context, but not the context itself.
4221      $usercontext->delete_content();
4222  
4223      // Delete any search data.
4224      \core_search\manager::context_deleted($usercontext);
4225  
4226      // Any plugin that needs to cleanup should register this event.
4227      // Trigger event.
4228      $event = \core\event\user_deleted::create(
4229              array(
4230                  'objectid' => $user->id,
4231                  'relateduserid' => $user->id,
4232                  'context' => $usercontext,
4233                  'other' => array(
4234                      'username' => $user->username,
4235                      'email' => $user->email,
4236                      'idnumber' => $user->idnumber,
4237                      'picture' => $user->picture,
4238                      'mnethostid' => $user->mnethostid
4239                      )
4240                  )
4241              );
4242      $event->add_record_snapshot('user', $olduser);
4243      $event->trigger();
4244  
4245      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4246      // should know about this updated property persisted to the user's table.
4247      $user->timemodified = $updateuser->timemodified;
4248  
4249      // Notify auth plugin - do not block the delete even when plugin fails.
4250      $authplugin = get_auth_plugin($user->auth);
4251      $authplugin->user_delete($user);
4252  
4253      return true;
4254  }
4255  
4256  /**
4257   * Retrieve the guest user object.
4258   *
4259   * @return stdClass A {@link $USER} object
4260   */
4261  function guest_user() {
4262      global $CFG, $DB;
4263  
4264      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4265          $newuser->confirmed = 1;
4266          $newuser->lang = get_newuser_language();
4267          $newuser->lastip = getremoteaddr();
4268      }
4269  
4270      return $newuser;
4271  }
4272  
4273  /**
4274   * Authenticates a user against the chosen authentication mechanism
4275   *
4276   * Given a username and password, this function looks them
4277   * up using the currently selected authentication mechanism,
4278   * and if the authentication is successful, it returns a
4279   * valid $user object from the 'user' table.
4280   *
4281   * Uses auth_ functions from the currently active auth module
4282   *
4283   * After authenticate_user_login() returns success, you will need to
4284   * log that the user has logged in, and call complete_user_login() to set
4285   * the session up.
4286   *
4287   * Note: this function works only with non-mnet accounts!
4288   *
4289   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4290   * @param string $password  User's password
4291   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4292   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4293   * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
4294   * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
4295   * @return stdClass|false A {@link $USER} object or false if error
4296   */
4297  function authenticate_user_login(
4298      $username,
4299      $password,
4300      $ignorelockout = false,
4301      &$failurereason = null,
4302      $logintoken = false,
4303      string|bool $loginrecaptcha = false,
4304  ) {
4305      global $CFG, $DB, $PAGE, $SESSION;
4306      require_once("$CFG->libdir/authlib.php");
4307  
4308      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4309          // we have found the user
4310  
4311      } else if (!empty($CFG->authloginviaemail)) {
4312          if ($email = clean_param($username, PARAM_EMAIL)) {
4313              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4314              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4315              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4316              if (count($users) === 1) {
4317                  // Use email for login only if unique.
4318                  $user = reset($users);
4319                  $user = get_complete_user_data('id', $user->id);
4320                  $username = $user->username;
4321              }
4322              unset($users);
4323          }
4324      }
4325  
4326      // Make sure this request came from the login form.
4327      if (!\core\session\manager::validate_login_token($logintoken)) {
4328          $failurereason = AUTH_LOGIN_FAILED;
4329  
4330          // Trigger login failed event (specifying the ID of the found user, if available).
4331          \core\event\user_login_failed::create([
4332              'userid' => ($user->id ?? 0),
4333              'other' => [
4334                  'username' => $username,
4335                  'reason' => $failurereason,
4336              ],
4337          ])->trigger();
4338  
4339          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4340          return false;
4341      }
4342  
4343      // Login reCaptcha.
4344      if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
4345          $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
4346          // Trigger login failed event (specifying the ID of the found user, if available).
4347          \core\event\user_login_failed::create([
4348              'userid' => ($user->id ?? 0),
4349              'other' => [
4350                  'username' => $username,
4351                  'reason' => $failurereason,
4352              ],
4353          ])->trigger();
4354          return false;
4355      }
4356  
4357      $authsenabled = get_enabled_auth_plugins();
4358  
4359      if ($user) {
4360          // Use manual if auth not set.
4361          $auth = empty($user->auth) ? 'manual' : $user->auth;
4362  
4363          if (in_array($user->auth, $authsenabled)) {
4364              $authplugin = get_auth_plugin($user->auth);
4365              $authplugin->pre_user_login_hook($user);
4366          }
4367  
4368          if (!empty($user->suspended)) {
4369              $failurereason = AUTH_LOGIN_SUSPENDED;
4370  
4371              // Trigger login failed event.
4372              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4373                      'other' => array('username' => $username, 'reason' => $failurereason)));
4374              $event->trigger();
4375              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4376              return false;
4377          }
4378          if ($auth=='nologin' or !is_enabled_auth($auth)) {
4379              // Legacy way to suspend user.
4380              $failurereason = AUTH_LOGIN_SUSPENDED;
4381  
4382              // Trigger login failed event.
4383              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4384                      'other' => array('username' => $username, 'reason' => $failurereason)));
4385              $event->trigger();
4386              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4387              return false;
4388          }
4389          $auths = array($auth);
4390  
4391      } else {
4392          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4393          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4394              $failurereason = AUTH_LOGIN_NOUSER;
4395  
4396              // Trigger login failed event.
4397              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4398                      'reason' => $failurereason)));
4399              $event->trigger();
4400              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4401              return false;
4402          }
4403  
4404          // User does not exist.
4405          $auths = $authsenabled;
4406          $user = new stdClass();
4407          $user->id = 0;
4408      }
4409  
4410      if ($ignorelockout) {
4411          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4412          // or this function is called from a SSO script.
4413      } else if ($user->id) {
4414          // Verify login lockout after other ways that may prevent user login.
4415          if (login_is_lockedout($user)) {
4416              $failurereason = AUTH_LOGIN_LOCKOUT;
4417  
4418              // Trigger login failed event.
4419              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4420                      'other' => array('username' => $username, 'reason' => $failurereason)));
4421              $event->trigger();
4422  
4423              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4424              $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
4425  
4426              return false;
4427          }
4428      } else {
4429          // We can not lockout non-existing accounts.
4430      }
4431  
4432      foreach ($auths as $auth) {
4433          $authplugin = get_auth_plugin($auth);
4434  
4435          // On auth fail fall through to the next plugin.
4436          if (!$authplugin->user_login($username, $password)) {
4437              continue;
4438          }
4439  
4440          // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4441          if (!empty($CFG->passwordpolicycheckonlogin)) {
4442              $errmsg = '';
4443              $passed = check_password_policy($password, $errmsg, $user);
4444              if (!$passed) {
4445                  // First trigger event for failure.
4446                  $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4447                  $failedevent->trigger();
4448  
4449                  // If able to change password, set flag and move on.
4450                  if ($authplugin->can_change_password()) {
4451                      // Check if we are on internal change password page, or service is external, don't show notification.
4452                      $internalchangeurl = new moodle_url('/login/change_password.php');
4453                      if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4454                          \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4455                      }
4456                      set_user_preference('auth_forcepasswordchange', 1, $user);
4457                  } else if ($authplugin->can_reset_password()) {
4458                      // Else force a reset if possible.
4459                      \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4460                      redirect(new moodle_url('/login/forgot_password.php'));
4461                  } else {
4462                      $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4463                      // If support page is set, add link for help.
4464                      if (!empty($CFG->supportpage)) {
4465                          $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4466                          $link = \html_writer::tag('p', $link);
4467                          $notifymsg .= $link;
4468                      }
4469  
4470                      // If no change or reset is possible, add a notification for user.
4471                      \core\notification::error($notifymsg);
4472                  }
4473              }
4474          }
4475  
4476          // Successful authentication.
4477          if ($user->id) {
4478              // User already exists in database.
4479              if (empty($user->auth)) {
4480                  // For some reason auth isn't set yet.
4481                  $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4482                  $user->auth = $auth;
4483              }
4484  
4485              // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4486              // the current hash algorithm while we have access to the user's password.
4487              update_internal_user_password($user, $password);
4488  
4489              if ($authplugin->is_synchronised_with_external()) {
4490                  // Update user record from external DB.
4491                  $user = update_user_record_by_id($user->id);
4492              }
4493          } else {
4494              // The user is authenticated but user creation may be disabled.
4495              if (!empty($CFG->authpreventaccountcreation)) {
4496                  $failurereason = AUTH_LOGIN_UNAUTHORISED;
4497  
4498                  // Trigger login failed event.
4499                  $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4500                          'reason' => $failurereason)));
4501                  $event->trigger();
4502  
4503                  error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4504                          $_SERVER['HTTP_USER_AGENT']);
4505                  return false;
4506              } else {
4507                  $user = create_user_record($username, $password, $auth);
4508              }
4509          }
4510  
4511          $authplugin->sync_roles($user);
4512  
4513          foreach ($authsenabled as $hau) {
4514              $hauth = get_auth_plugin($hau);
4515              $hauth->user_authenticated_hook($user, $username, $password);
4516          }
4517  
4518          if (empty($user->id)) {
4519              $failurereason = AUTH_LOGIN_NOUSER;
4520              // Trigger login failed event.
4521              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4522                      'reason' => $failurereason)));
4523              $event->trigger();
4524              return false;
4525          }
4526  
4527          if (!empty($user->suspended)) {
4528              // Just in case some auth plugin suspended account.
4529              $failurereason = AUTH_LOGIN_SUSPENDED;
4530              // Trigger login failed event.
4531              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4532                      'other' => array('username' => $username, 'reason' => $failurereason)));
4533              $event->trigger();
4534              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4535              return false;
4536          }
4537  
4538          login_attempt_valid($user);
4539          $failurereason = AUTH_LOGIN_OK;
4540          return $user;
4541      }
4542  
4543      // Failed if all the plugins have failed.
4544      if (debugging('', DEBUG_ALL)) {
4545          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4546      }
4547  
4548      if ($user->id) {
4549          login_attempt_failed($user);
4550          $failurereason = AUTH_LOGIN_FAILED;
4551          // Trigger login failed event.
4552          $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4553                  'other' => array('username' => $username, 'reason' => $failurereason)));
4554          $event->trigger();
4555      } else {
4556          $failurereason = AUTH_LOGIN_NOUSER;
4557          // Trigger login failed event.
4558          $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4559                  'reason' => $failurereason)));
4560          $event->trigger();
4561      }
4562  
4563      return false;
4564  }
4565  
4566  /**
4567   * Call to complete the user login process after authenticate_user_login()
4568   * has succeeded. It will setup the $USER variable and other required bits
4569   * and pieces.
4570   *
4571   * NOTE:
4572   * - It will NOT log anything -- up to the caller to decide what to log.
4573   * - this function does not set any cookies any more!
4574   *
4575   * @param stdClass $user
4576   * @param array $extrauserinfo
4577   * @return stdClass A {@link $USER} object - BC only, do not use
4578   */
4579  function complete_user_login($user, array $extrauserinfo = []) {
4580      global $CFG, $DB, $USER, $SESSION;
4581  
4582      \core\session\manager::login_user($user);
4583  
4584      // Reload preferences from DB.
4585      unset($USER->preference);
4586      check_user_preferences_loaded($USER);
4587  
4588      // Update login times.
4589      update_user_login_times();
4590  
4591      // Extra session prefs init.
4592      set_login_session_preferences();
4593  
4594      // Trigger login event.
4595      $event = \core\event\user_loggedin::create(
4596          array(
4597              'userid' => $USER->id,
4598              'objectid' => $USER->id,
4599              'other' => [
4600                  'username' => $USER->username,
4601                  'extrauserinfo' => $extrauserinfo
4602              ]
4603          )
4604      );
4605      $event->trigger();
4606  
4607      // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4608      // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4609      // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4610      $loginip = getremoteaddr();
4611      $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4612      $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4613  
4614      if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4615  
4616          $logintime = time();
4617          $ismoodleapp = false;
4618          $useragent = \core_useragent::get_user_agent_string();
4619  
4620          // Schedule adhoc task to sent a login notification to the user.
4621          $task = new \core\task\send_login_notifications();
4622          $task->set_userid($USER->id);
4623          $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4624          $task->set_component('core');
4625          \core\task\manager::queue_adhoc_task($task);
4626      }
4627  
4628      // Queue migrating the messaging data, if we need to.
4629      if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4630          // Check if there are any legacy messages to migrate.
4631          if (\core_message\helper::legacy_messages_exist($USER->id)) {
4632              \core_message\task\migrate_message_data::queue_task($USER->id);
4633          } else {
4634              set_user_preference('core_message_migrate_data', true, $USER->id);
4635          }
4636      }
4637  
4638      if (isguestuser()) {
4639          // No need to continue when user is THE guest.
4640          return $USER;
4641      }
4642  
4643      if (CLI_SCRIPT) {
4644          // We can redirect to password change URL only in browser.
4645          return $USER;
4646      }
4647  
4648      // Select password change url.
4649      $userauth = get_auth_plugin($USER->auth);
4650  
4651      // Check whether the user should be changing password.
4652      if (get_user_preferences('auth_forcepasswordchange', false)) {
4653          if ($userauth->can_change_password()) {
4654              if ($changeurl = $userauth->change_password_url()) {
4655                  redirect($changeurl);
4656              } else {
4657                  require_once($CFG->dirroot . '/login/lib.php');
4658                  $SESSION->wantsurl = core_login_get_return_url();
4659                  redirect($CFG->wwwroot.'/login/change_password.php');
4660              }
4661          } else {
4662              throw new \moodle_exception('nopasswordchangeforced', 'auth');
4663          }
4664      }
4665      return $USER;
4666  }
4667  
4668  /**
4669   * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4670   *
4671   * @param string $password String to check.
4672   * @return bool True if the $password matches the format of a bcrypt hash.
4673   */
4674  function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
4675      return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4676  }
4677  
4678  /**
4679   * Calculate the Shannon entropy of a string.
4680   *
4681   * @param string $pepper The pepper to calculate the entropy of.
4682   * @return float The Shannon entropy of the string.
4683   */
4684  function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
4685      // Initialize entropy.
4686      $h = 0;
4687  
4688      // Calculate the length of the string.
4689      $size = strlen($pepper);
4690  
4691      // For each unique character in the string.
4692      foreach (count_chars($pepper, 1) as $v) {
4693          // Calculate the probability of the character.
4694          $p = $v / $size;
4695  
4696          // Add the character's contribution to the total entropy.
4697          // This uses the formula for the entropy of a discrete random variable.
4698          $h -= $p * log($p) / log(2);
4699      }
4700  
4701      // Instead of returning the average entropy per symbol (Shannon entropy),
4702      // we multiply by the length of the string to get total entropy.
4703      return $h * $size;
4704  }
4705  
4706  /**
4707   * Get the available password peppers.
4708   * The latest pepper is checked for minimum entropy as part of this function.
4709   * We only calculate the entropy of the most recent pepper,
4710   * because passwords are always updated to the latest pepper,
4711   * and in the past we may have enforced a lower minimum entropy.
4712   * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4713   *
4714   * @return array The password peppers.
4715   * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4716   */
4717  function get_password_peppers(): array {
4718      global $CFG;
4719  
4720      // Get all available peppers.
4721      if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4722          // Sort the array in descending order of keys (numerical).
4723          $peppers = $CFG->passwordpeppers;
4724          krsort($peppers, SORT_NUMERIC);
4725      } else {
4726          $peppers = [];  // Set an empty array if no peppers are found.
4727      }
4728  
4729      // Check if the entropy of the most recent pepper is less than the minimum.
4730      // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4731      $lastpepper = reset($peppers);
4732      if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4733          throw new coding_exception(
4734                  'password pepper below minimum',
4735                  'The entropy of the password pepper is less than the recommended minimum.');
4736      }
4737      return $peppers;
4738  }
4739  
4740  /**
4741   * Compare password against hash stored in user object to determine if it is valid.
4742   *
4743   * If necessary it also updates the stored hash to the current format.
4744   *
4745   * @param stdClass $user (Password property may be updated).
4746   * @param string $password Plain text password.
4747   * @return bool True if password is valid.
4748   */
4749  function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool {
4750  
4751      if (exceeds_password_length($password)) {
4752          // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4753          return false;
4754      }
4755  
4756      if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4757          // Internal password is not used at all, it can not validate.
4758          return false;
4759      }
4760  
4761      $peppers = get_password_peppers(); // Get the array of available peppers.
4762      $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4763  
4764      // If the password is a legacy hash, no peppers were used, so verify and update directly.
4765      if ($islegacy && password_verify($password, $user->password)) {
4766          update_internal_user_password($user, $password);
4767          return true;
4768      }
4769  
4770      // If the password is not a legacy hash, iterate through the peppers.
4771      $latestpepper = reset($peppers);
4772      // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4773      $peppers = [-1 => ''] + $peppers;
4774      foreach ($peppers as $pepper) {
4775          $pepperedpassword = $password . $pepper;
4776  
4777          // If the peppered password is correct, update (if necessary) and return true.
4778          if (password_verify($pepperedpassword, $user->password)) {
4779              // If the pepper used is not the latest one, update the password.
4780              if ($pepper !== $latestpepper) {
4781                  update_internal_user_password($user, $password);
4782              }
4783              return true;
4784          }
4785      }
4786  
4787      // If no peppered password was correct, the password is wrong.
4788      return false;
4789  }
4790  
4791  /**
4792   * Calculate hash for a plain text password.
4793   *
4794   * @param string $password Plain text password to be hashed.
4795   * @param bool $fasthash If true, use a low number of rounds when generating the hash
4796   *                       This is faster to generate but makes the hash less secure.
4797   *                       It is used when lots of hashes need to be generated quickly.
4798   * @param int $pepperlength Lenght of the peppers
4799   * @return string The hashed password.
4800   *
4801   * @throws moodle_exception If a problem occurs while generating the hash.
4802   */
4803  function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
4804      if (exceeds_password_length($password, $pepperlength)) {
4805          // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4806          throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4807      }
4808  
4809      // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4810      $rounds = $fasthash ? 5000 : 10000;
4811  
4812      // First generate a cryptographically suitable salt.
4813      $randombytes = random_bytes(16);
4814      $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4815  
4816      // Now construct the password string with the salt and number of rounds.
4817      // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4818      $generatedhash = crypt($password, implode('$', [
4819          '',
4820          // The SHA512 Algorithm
4821          '6',
4822          "rounds={$rounds}",
4823          $salt,
4824          '',
4825      ]));
4826  
4827      if ($generatedhash === false || $generatedhash === null) {
4828          throw new moodle_exception('Failed to generate password hash.');
4829      }
4830  
4831      return $generatedhash;
4832  }
4833  
4834  /**
4835   * Update password hash in user object (if necessary).
4836   *
4837   * The password is updated if:
4838   * 1. The password has changed (the hash of $user->password is different
4839   *    to the hash of $password).
4840   * 2. The existing hash is using an out-of-date algorithm (or the legacy
4841   *    md5 algorithm).
4842   *
4843   * The password is peppered with the latest pepper before hashing,
4844   * if peppers are available.
4845   * Updating the password will modify the $user object and the database
4846   * record to use the current hashing algorithm.
4847   * It will remove Web Services user tokens too.
4848   *
4849   * @param stdClass $user User object (password property may be updated).
4850   * @param string $password Plain text password.
4851   * @param bool $fasthash If true, use a low cost factor when generating the hash
4852   *                       This is much faster to generate but makes the hash
4853   *                       less secure. It is used when lots of hashes need to
4854   *                       be generated quickly.
4855   * @return bool Always returns true.
4856   */
4857  function update_internal_user_password(
4858          stdClass $user,
4859          #[\SensitiveParameter] string $password,
4860          bool $fasthash = false
4861  ): bool {
4862      global $CFG, $DB;
4863  
4864      // Add the latest password pepper to the password before further processing.
4865      $peppers = get_password_peppers();
4866      if (!empty($peppers)) {
4867          $password = $password . reset($peppers);
4868      }
4869  
4870      // Figure out what the hashed password should be.
4871      if (!isset($user->auth)) {
4872          debugging('User record in update_internal_user_password() must include field auth',
4873                  DEBUG_DEVELOPER);
4874          $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4875      }
4876      $authplugin = get_auth_plugin($user->auth);
4877      if ($authplugin->prevent_local_passwords()) {
4878          $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4879      } else {
4880          $hashedpassword = hash_internal_user_password($password, $fasthash);
4881      }
4882  
4883      $algorithmchanged = false;
4884  
4885      if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4886          // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4887          $passwordchanged = ($user->password !== $hashedpassword);
4888  
4889      } else if (isset($user->password)) {
4890          // If verification fails then it means the password has changed.
4891          $passwordchanged = !password_verify($password, $user->password);
4892          $algorithmchanged = password_is_legacy_hash($user->password);
4893      } else {
4894          // While creating new user, password in unset in $user object, to avoid
4895          // saving it with user_create()
4896          $passwordchanged = true;
4897      }
4898  
4899      if ($passwordchanged || $algorithmchanged) {
4900          $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4901          $user->password = $hashedpassword;
4902  
4903          // Trigger event.
4904          $user = $DB->get_record('user', array('id' => $user->id));
4905          \core\event\user_password_updated::create_from_user($user)->trigger();
4906  
4907          // Remove WS user tokens.
4908          if (!empty($CFG->passwordchangetokendeletion)) {
4909              require_once($CFG->dirroot.'/webservice/lib.php');
4910              webservice::delete_user_ws_tokens($user->id);
4911          }
4912      }
4913  
4914      return true;
4915  }
4916  
4917  /**
4918   * Get a complete user record, which includes all the info in the user record.
4919   *
4920   * Intended for setting as $USER session variable
4921   *
4922   * @param string $field The user field to be checked for a given value.
4923   * @param string $value The value to match for $field.
4924   * @param int $mnethostid
4925   * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4926   *                              found. Otherwise, it will just return false.
4927   * @return mixed False, or A {@link $USER} object.
4928   */
4929  function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4930      global $CFG, $DB;
4931  
4932      if (!$field || !$value) {
4933          return false;
4934      }
4935  
4936      // Change the field to lowercase.
4937      $field = core_text::strtolower($field);
4938  
4939      // List of case insensitive fields.
4940      $caseinsensitivefields = ['email'];
4941  
4942      // Username input is forced to lowercase and should be case sensitive.
4943      if ($field == 'username') {
4944          $value = core_text::strtolower($value);
4945      }
4946  
4947      // Build the WHERE clause for an SQL query.
4948      $params = array('fieldval' => $value);
4949  
4950      // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4951      // such as MySQL by pre-filtering users with accent-insensitive subselect.
4952      if (in_array($field, $caseinsensitivefields)) {
4953          $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4954          $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4955          $params['fieldval2'] = $value;
4956      } else {
4957          $fieldselect = "$field = :fieldval";
4958          $idsubselect = '';
4959      }
4960      $constraints = "$fieldselect AND deleted <> 1";
4961  
4962      // If we are loading user data based on anything other than id,
4963      // we must also restrict our search based on mnet host.
4964      if ($field != 'id') {
4965          if (empty($mnethostid)) {
4966              // If empty, we restrict to local users.
4967              $mnethostid = $CFG->mnet_localhost_id;
4968          }
4969      }
4970      if (!empty($mnethostid)) {
4971          $params['mnethostid'] = $mnethostid;
4972          $constraints .= " AND mnethostid = :mnethostid";
4973      }
4974  
4975      if ($idsubselect) {
4976          $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4977      }
4978  
4979      // Get all the basic user data.
4980      try {
4981          // Make sure that there's only a single record that matches our query.
4982          // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4983          // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4984          $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4985      } catch (dml_exception $exception) {
4986          if ($throwexception) {
4987              throw $exception;
4988          } else {
4989              // Return false when no records or multiple records were found.
4990              return false;
4991          }
4992      }
4993  
4994      // Get various settings and preferences.
4995  
4996      // Preload preference cache.
4997      check_user_preferences_loaded($user);
4998  
4999      // Load course enrolment related stuff.
5000      $user->lastcourseaccess    = array(); // During last session.
5001      $user->currentcourseaccess = array(); // During current session.
5002      if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
5003          foreach ($lastaccesses as $lastaccess) {
5004              $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
5005          }
5006      }
5007  
5008      // Add cohort theme.
5009      if (!empty($CFG->allowcohortthemes)) {
5010          require_once($CFG->dirroot . '/cohort/lib.php');
5011          if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
5012              $user->cohorttheme = $cohorttheme;
5013          }
5014      }
5015  
5016      // Add the custom profile fields to the user record.
5017      $user->profile = array();
5018      if (!isguestuser($user)) {
5019          require_once($CFG->dirroot.'/user/profile/lib.php');
5020          profile_load_custom_fields($user);
5021      }
5022  
5023      // Rewrite some variables if necessary.
5024      if (!empty($user->description)) {
5025          // No need to cart all of it around.
5026          $user->description = true;
5027      }
5028      if (isguestuser($user)) {
5029          // Guest language always same as site.
5030          $user->lang = get_newuser_language();
5031          // Name always in current language.
5032          $user->firstname = get_string('guestuser');
5033          $user->lastname = ' ';
5034      }
5035  
5036      return $user;
5037  }
5038  
5039  /**
5040   * Validate a password against the configured password policy
5041   *
5042   * @param string $password the password to be checked against the password policy
5043   * @param string $errmsg the error message to display when the password doesn't comply with the policy.
5044   * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
5045   *
5046   * @return bool true if the password is valid according to the policy. false otherwise.
5047   */
5048  function check_password_policy($password, &$errmsg, $user = null) {
5049      global $CFG;
5050  
5051      if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
5052          $errmsg = '';
5053          if (core_text::strlen($password) < $CFG->minpasswordlength) {
5054              $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
5055          }
5056          if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
5057              $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
5058          }
5059          if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
5060              $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
5061          }
5062          if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
5063              $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
5064          }
5065          if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
5066              $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
5067          }
5068          if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
5069              $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
5070          }
5071  
5072          // Fire any additional password policy functions from plugins.
5073          // Plugin functions should output an error message string or empty string for success.
5074          $pluginsfunction = get_plugins_with_function('check_password_policy');
5075          foreach ($pluginsfunction as $plugintype => $plugins) {
5076              foreach ($plugins as $pluginfunction) {
5077                  $pluginerr = $pluginfunction($password, $user);
5078                  if ($pluginerr) {
5079                      $errmsg .= '<div>'. $pluginerr .'</div>';
5080                  }
5081              }
5082          }
5083      }
5084  
5085      if ($errmsg == '') {
5086          return true;
5087      } else {
5088          return false;
5089      }
5090  }
5091  
5092  
5093  /**
5094   * When logging in, this function is run to set certain preferences for the current SESSION.
5095   */
5096  function set_login_session_preferences() {
5097      global $SESSION;
5098  
5099      $SESSION->justloggedin = true;
5100  
5101      unset($SESSION->lang);
5102      unset($SESSION->forcelang);
5103      unset($SESSION->load_navigation_admin);
5104  }
5105  
5106  
5107  /**
5108   * Delete a course, including all related data from the database, and any associated files.
5109   *
5110   * @param mixed $courseorid The id of the course or course object to delete.
5111   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5112   * @return bool true if all the removals succeeded. false if there were any failures. If this
5113   *             method returns false, some of the removals will probably have succeeded, and others
5114   *             failed, but you have no way of knowing which.
5115   */
5116  function delete_course($courseorid, $showfeedback = true) {
5117      global $DB, $CFG;
5118  
5119      if (is_object($courseorid)) {
5120          $courseid = $courseorid->id;
5121          $course   = $courseorid;
5122      } else {
5123          $courseid = $courseorid;
5124          if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5125              return false;
5126          }
5127      }
5128      $context = context_course::instance($courseid);
5129  
5130      // Frontpage course can not be deleted!!
5131      if ($courseid == SITEID) {
5132          return false;
5133      }
5134  
5135      // Allow plugins to use this course before we completely delete it.
5136      if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5137          foreach ($pluginsfunction as $plugintype => $plugins) {
5138              foreach ($plugins as $pluginfunction) {
5139                  $pluginfunction($course);
5140              }
5141          }
5142      }
5143  
5144      // Tell the search manager we are about to delete a course. This prevents us sending updates
5145      // for each individual context being deleted.
5146      \core_search\manager::course_deleting_start($courseid);
5147  
5148      $handler = core_course\customfield\course_handler::create();
5149      $handler->delete_instance($courseid);
5150  
5151      // Make the course completely empty.
5152      remove_course_contents($courseid, $showfeedback);
5153  
5154      // Communication provider delete associated information.
5155      $communication = \core_communication\api::load_by_instance(
5156          $context,
5157          'core_course',
5158          'coursecommunication',
5159          $course->id
5160      );
5161  
5162      // Delete the course and related context instance.
5163      context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5164  
5165      // Update communication room membership of enrolled users.
5166      require_once($CFG->libdir . '/enrollib.php');
5167      $courseusers = enrol_get_course_users($courseid);
5168      $enrolledusers = [];
5169  
5170      foreach ($courseusers as $user) {
5171          $enrolledusers[] = $user->id;
5172      }
5173  
5174      $communication->remove_members_from_room($enrolledusers);
5175  
5176      $communication->delete_room();
5177  
5178      $DB->delete_records("course", array("id" => $courseid));
5179      $DB->delete_records("course_format_options", array("courseid" => $courseid));
5180  
5181      // Reset all course related caches here.
5182      core_courseformat\base::reset_course_cache($courseid);
5183  
5184      // Tell search that we have deleted the course so it can delete course data from the index.
5185      \core_search\manager::course_deleting_finish($courseid);
5186  
5187      // Trigger a course deleted event.
5188      $event = \core\event\course_deleted::create(array(
5189          'objectid' => $course->id,
5190          'context' => $context,
5191          'other' => array(
5192              'shortname' => $course->shortname,
5193              'fullname' => $course->fullname,
5194              'idnumber' => $course->idnumber
5195              )
5196      ));
5197      $event->add_record_snapshot('course', $course);
5198      $event->trigger();
5199  
5200      return true;
5201  }
5202  
5203  /**
5204   * Clear a course out completely, deleting all content but don't delete the course itself.
5205   *
5206   * This function does not verify any permissions.
5207   *
5208   * Please note this function also deletes all user enrolments,
5209   * enrolment instances and role assignments by default.
5210   *
5211   * $options:
5212   *  - 'keep_roles_and_enrolments' - false by default
5213   *  - 'keep_groups_and_groupings' - false by default
5214   *
5215   * @param int $courseid The id of the course that is being deleted
5216   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5217   * @param array $options extra options
5218   * @return bool true if all the removals succeeded. false if there were any failures. If this
5219   *             method returns false, some of the removals will probably have succeeded, and others
5220   *             failed, but you have no way of knowing which.
5221   */
5222  function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5223      global $CFG, $DB, $OUTPUT;
5224  
5225      require_once($CFG->libdir.'/badgeslib.php');
5226      require_once($CFG->libdir.'/completionlib.php');
5227      require_once($CFG->libdir.'/questionlib.php');
5228      require_once($CFG->libdir.'/gradelib.php');
5229      require_once($CFG->dirroot.'/group/lib.php');
5230      require_once($CFG->dirroot.'/comment/lib.php');
5231      require_once($CFG->dirroot.'/rating/lib.php');
5232      require_once($CFG->dirroot.'/notes/lib.php');
5233  
5234      // Handle course badges.
5235      badges_handle_course_deletion($courseid);
5236  
5237      // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5238      $strdeleted = get_string('deleted').' - ';
5239  
5240      // Some crazy wishlist of stuff we should skip during purging of course content.
5241      $options = (array)$options;
5242  
5243      $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5244      $coursecontext = context_course::instance($courseid);
5245      $fs = get_file_storage();
5246  
5247      // Delete course completion information, this has to be done before grades and enrols.
5248      $cc = new completion_info($course);
5249      $cc->clear_criteria();
5250      if ($showfeedback) {
5251          echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5252      }
5253  
5254      // Remove all data from gradebook - this needs to be done before course modules
5255      // because while deleting this information, the system may need to reference
5256      // the course modules that own the grades.
5257      remove_course_grades($courseid, $showfeedback);
5258      remove_grade_letters($coursecontext, $showfeedback);
5259  
5260      // Delete course blocks in any all child contexts,
5261      // they may depend on modules so delete them first.
5262      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5263      foreach ($childcontexts as $childcontext) {
5264          blocks_delete_all_for_context($childcontext->id);
5265      }
5266      unset($childcontexts);
5267      blocks_delete_all_for_context($coursecontext->id);
5268      if ($showfeedback) {
5269          echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5270      }
5271  
5272      $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5273      rebuild_course_cache($courseid, true);
5274  
5275      // Get the list of all modules that are properly installed.
5276      $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5277  
5278      // Delete every instance of every module,
5279      // this has to be done before deleting of course level stuff.
5280      $locations = core_component::get_plugin_list('mod');
5281      foreach ($locations as $modname => $moddir) {
5282          if ($modname === 'NEWMODULE') {
5283              continue;
5284          }
5285          if (array_key_exists($modname, $allmodules)) {
5286              $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5287                FROM {".$modname."} m
5288                     LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5289               WHERE m.course = :courseid";
5290              $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5291                  'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5292  
5293              include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
5294              $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
5295  
5296              if ($instances) {
5297                  foreach ($instances as $cm) {
5298                      if ($cm->id) {
5299                          // Delete activity context questions and question categories.
5300                          question_delete_activity($cm);
5301                          // Notify the competency subsystem.
5302                          \core_competency\api::hook_course_module_deleted($cm);
5303  
5304                          // Delete all tag instances associated with the instance of this module.
5305                          core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
5306                          core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
5307                      }
5308                      if (function_exists($moddelete)) {
5309                          // This purges all module data in related tables, extra user prefs, settings, etc.
5310                          $moddelete($cm->modinstance);
5311                      } else {
5312                          // NOTE: we should not allow installation of modules with missing delete support!
5313                          debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5314                          $DB->delete_records($modname, array('id' => $cm->modinstance));
5315                      }
5316  
5317                      if ($cm->id) {
5318                          // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5319                          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5320                          $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5321                          $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
5322                          $DB->delete_records('course_modules', array('id' => $cm->id));
5323                          rebuild_course_cache($cm->course, true);
5324                      }
5325                  }
5326              }
5327              if ($instances and $showfeedback) {
5328                  echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5329              }
5330          } else {
5331              // Ooops, this module is not properly installed, force-delete it in the next block.
5332          }
5333      }
5334  
5335      // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5336  
5337      // Delete completion defaults.
5338      $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5339  
5340      // Remove all data from availability and completion tables that is associated
5341      // with course-modules belonging to this course. Note this is done even if the
5342      // features are not enabled now, in case they were enabled previously.
5343      $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5344              'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5345      $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
5346          'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5347  
5348      // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5349      $cms = $DB->get_records('course_modules', array('course' => $course->id));
5350      $allmodulesbyid = array_flip($allmodules);
5351      foreach ($cms as $cm) {
5352          if (array_key_exists($cm->module, $allmodulesbyid)) {
5353              try {
5354                  $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5355              } catch (Exception $e) {
5356                  // Ignore weird or missing table problems.
5357              }
5358          }
5359          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5360          $DB->delete_records('course_modules', array('id' => $cm->id));
5361          rebuild_course_cache($cm->course, true);
5362      }
5363  
5364      if ($showfeedback) {
5365          echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5366      }
5367  
5368      // Delete questions and question categories.
5369      question_delete_course($course);
5370      if ($showfeedback) {
5371          echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5372      }
5373  
5374      // Delete content bank contents.
5375      $cb = new \core_contentbank\contentbank();
5376      $cbdeleted = $cb->delete_contents($coursecontext);
5377      if ($showfeedback && $cbdeleted) {
5378          echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5379      }
5380  
5381      // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5382      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5383      foreach ($childcontexts as $childcontext) {
5384          $childcontext->delete();
5385      }
5386      unset($childcontexts);
5387  
5388      // Remove roles and enrolments by default.
5389      if (empty($options['keep_roles_and_enrolments'])) {
5390          // This hack is used in restore when deleting contents of existing course.
5391          // During restore, we should remove only enrolment related data that the user performing the restore has a
5392          // permission to remove.
5393          $userid = $options['userid'] ?? null;
5394          enrol_course_delete($course, $userid);
5395          role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5396          if ($showfeedback) {
5397              echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5398          }
5399      }
5400  
5401      // Delete any groups, removing members and grouping/course links first.
5402      if (empty($options['keep_groups_and_groupings'])) {
5403          groups_delete_groupings($course->id, $showfeedback);
5404          groups_delete_groups($course->id, $showfeedback);
5405      }
5406  
5407      // Filters be gone!
5408      filter_delete_all_for_context($coursecontext->id);
5409  
5410      // Notes, you shall not pass!
5411      note_delete_all($course->id);
5412  
5413      // Die comments!
5414      comment::delete_comments($coursecontext->id);
5415  
5416      // Ratings are history too.
5417      $delopt = new stdclass();
5418      $delopt->contextid = $coursecontext->id;
5419      $rm = new rating_manager();
5420      $rm->delete_ratings($delopt);
5421  
5422      // Delete course tags.
5423      core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5424  
5425      // Give the course format the opportunity to remove its obscure data.
5426      $format = course_get_format($course);
5427      $format->delete_format_data();
5428  
5429      // Notify the competency subsystem.
5430      \core_competency\api::hook_course_deleted($course);
5431  
5432      // Delete calendar events.
5433      $DB->delete_records('event', array('courseid' => $course->id));
5434      $fs->delete_area_files($coursecontext->id, 'calendar');
5435  
5436      // Delete all related records in other core tables that may have a courseid
5437      // This array stores the tables that need to be cleared, as
5438      // table_name => column_name that contains the course id.
5439      $tablestoclear = array(
5440          'backup_courses' => 'courseid',  // Scheduled backup stuff.
5441          'user_lastaccess' => 'courseid', // User access info.
5442      );
5443      foreach ($tablestoclear as $table => $col) {
5444          $DB->delete_records($table, array($col => $course->id));
5445      }
5446  
5447      // Delete all course backup files.
5448      $fs->delete_area_files($coursecontext->id, 'backup');
5449  
5450      // Cleanup course record - remove links to deleted stuff.
5451      // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
5452      // increasing.
5453      $oldcourse = new stdClass();
5454      $oldcourse->id               = $course->id;
5455      $oldcourse->summary          = '';
5456      $oldcourse->legacyfiles      = 0;
5457      if (!empty($options['keep_groups_and_groupings'])) {
5458          $oldcourse->defaultgroupingid = 0;
5459      }
5460      $DB->update_record('course', $oldcourse);
5461  
5462      // Delete course sections.
5463      $DB->delete_records('course_sections', array('course' => $course->id));
5464  
5465      // Delete legacy, section and any other course files.
5466      $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5467  
5468      // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5469      if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5470          // Easy, do not delete the context itself...
5471          $coursecontext->delete_content();
5472      } else {
5473          // Hack alert!!!!
5474          // We can not drop all context stuff because it would bork enrolments and roles,
5475          // there might be also files used by enrol plugins...
5476      }
5477  
5478      // Delete legacy files - just in case some files are still left there after conversion to new file api,
5479      // also some non-standard unsupported plugins may try to store something there.
5480      fulldelete($CFG->dataroot.'/'.$course->id);
5481  
5482      // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5483      course_modinfo::purge_course_cache($courseid);
5484  
5485      // Trigger a course content deleted event.
5486      $event = \core\event\course_content_deleted::create(array(
5487          'objectid' => $course->id,
5488          'context' => $coursecontext,
5489          'other' => array('shortname' => $course->shortname,
5490                           'fullname' => $course->fullname,
5491                           'options' => $options) // Passing this for legacy reasons.
5492      ));
5493      $event->add_record_snapshot('course', $course);
5494      $event->trigger();
5495  
5496      return true;
5497  }
5498  
5499  /**
5500   * Change dates in module - used from course reset.
5501   *
5502   * @param string $modname forum, assignment, etc
5503   * @param array $fields array of date fields from mod table
5504   * @param int $timeshift time difference
5505   * @param int $courseid
5506   * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5507   * @return bool success
5508   */
5509  function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5510      global $CFG, $DB;
5511      include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5512  
5513      $return = true;
5514      $params = array($timeshift, $courseid);
5515      foreach ($fields as $field) {
5516          $updatesql = "UPDATE {".$modname."}
5517                            SET $field = $field + ?
5518                          WHERE course=? AND $field<>0";
5519          if ($modid) {
5520              $updatesql .= ' AND id=?';
5521              $params[] = $modid;
5522          }
5523          $return = $DB->execute($updatesql, $params) && $return;
5524      }
5525  
5526      return $return;
5527  }
5528  
5529  /**
5530   * This function will empty a course of user data.
5531   * It will retain the activities and the structure of the course.
5532   *
5533   * @param object $data an object containing all the settings including courseid (without magic quotes)
5534   * @return array status array of array component, item, error
5535   */
5536  function reset_course_userdata($data) {
5537      global $CFG, $DB;
5538      require_once($CFG->libdir.'/gradelib.php');
5539      require_once($CFG->libdir.'/completionlib.php');
5540      require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5541      require_once($CFG->dirroot.'/group/lib.php');
5542  
5543      $data->courseid = $data->id;
5544      $context = context_course::instance($data->courseid);
5545  
5546      $eventparams = array(
5547          'context' => $context,
5548          'courseid' => $data->id,
5549          'other' => array(
5550              'reset_options' => (array) $data
5551          )
5552      );
5553      $event = \core\event\course_reset_started::create($eventparams);
5554      $event->trigger();
5555  
5556      // Calculate the time shift of dates.
5557      if (!empty($data->reset_start_date)) {
5558          // Time part of course startdate should be zero.
5559          $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5560      } else {
5561          $data->timeshift = 0;
5562      }
5563  
5564      // Result array: component, item, error.
5565      $status = array();
5566  
5567      // Start the resetting.
5568      $componentstr = get_string('general');
5569  
5570      // Move the course start time.
5571      if (!empty($data->reset_start_date) and $data->timeshift) {
5572          // Change course start data.
5573          $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5574          // Update all course and group events - do not move activity events.
5575          $updatesql = "UPDATE {event}
5576                           SET timestart = timestart + ?
5577                         WHERE courseid=? AND instance=0";
5578          $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5579  
5580          // Update any date activity restrictions.
5581          if ($CFG->enableavailability) {
5582              \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5583          }
5584  
5585          // Update completion expected dates.
5586          if ($CFG->enablecompletion) {
5587              $modinfo = get_fast_modinfo($data->courseid);
5588              $changed = false;
5589              foreach ($modinfo->get_cms() as $cm) {
5590                  if ($cm->completion && !empty($cm->completionexpected)) {
5591                      $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5592                          array('id' => $cm->id));
5593                      $changed = true;
5594                  }
5595              }
5596  
5597              // Clear course cache if changes made.
5598              if ($changed) {
5599                  rebuild_course_cache($data->courseid, true);
5600              }
5601  
5602              // Update course date completion criteria.
5603              \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5604          }
5605  
5606          $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5607      }
5608  
5609      if (!empty($data->reset_end_date)) {
5610          // If the user set a end date value respect it.
5611          $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5612      } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5613          // If there is a time shift apply it to the end date as well.
5614          $enddate = $data->reset_end_date_old + $data->timeshift;
5615          $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5616      }
5617  
5618      if (!empty($data->reset_events)) {
5619          $DB->delete_records('event', array('courseid' => $data->courseid));
5620          $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5621      }
5622  
5623      if (!empty($data->reset_notes)) {
5624          require_once($CFG->dirroot.'/notes/lib.php');
5625          note_delete_all($data->courseid);
5626          $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5627      }
5628  
5629      if (!empty($data->delete_blog_associations)) {
5630          require_once($CFG->dirroot.'/blog/lib.php');
5631          blog_remove_associations_for_course($data->courseid);
5632          $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5633      }
5634  
5635      if (!empty($data->reset_completion)) {
5636          // Delete course and activity completion information.
5637          $course = $DB->get_record('course', array('id' => $data->courseid));
5638          $cc = new completion_info($course);
5639          $cc->delete_all_completion_data();
5640          $status[] = array('component' => $componentstr,
5641                  'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5642      }
5643  
5644      if (!empty($data->reset_competency_ratings)) {
5645          \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5646          $status[] = array('component' => $componentstr,
5647              'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5648      }
5649  
5650      $componentstr = get_string('roles');
5651  
5652      if (!empty($data->reset_roles_overrides)) {
5653          $children = $context->get_child_contexts();
5654          foreach ($children as $child) {
5655              $child->delete_capabilities();
5656          }
5657          $context->delete_capabilities();
5658          $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5659      }
5660  
5661      if (!empty($data->reset_roles_local)) {
5662          $children = $context->get_child_contexts();
5663          foreach ($children as $child) {
5664              role_unassign_all(array('contextid' => $child->id));
5665          }
5666          $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5667      }
5668  
5669      // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5670      $data->unenrolled = array();
5671      if (!empty($data->unenrol_users)) {
5672          $plugins = enrol_get_plugins(true);
5673          $instances = enrol_get_instances($data->courseid, true);
5674          foreach ($instances as $key => $instance) {
5675              if (!isset($plugins[$instance->enrol])) {
5676                  unset($instances[$key]);
5677                  continue;
5678              }
5679          }
5680  
5681          $usersroles = enrol_get_course_users_roles($data->courseid);
5682          foreach ($data->unenrol_users as $withroleid) {
5683              if ($withroleid) {
5684                  $sql = "SELECT ue.*
5685                            FROM {user_enrolments} ue
5686                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5687                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5688                            JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5689                  $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5690  
5691              } else {
5692                  // Without any role assigned at course context.
5693                  $sql = "SELECT ue.*
5694                            FROM {user_enrolments} ue
5695                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5696                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5697                       LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5698                           WHERE ra.id IS null";
5699                  $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5700              }
5701  
5702              $rs = $DB->get_recordset_sql($sql, $params);
5703              foreach ($rs as $ue) {
5704                  if (!isset($instances[$ue->enrolid])) {
5705                      continue;
5706                  }
5707                  $instance = $instances[$ue->enrolid];
5708                  $plugin = $plugins[$instance->enrol];
5709                  if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5710                      continue;
5711                  }
5712  
5713                  if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5714                      // If we don't remove all roles and user has more than one role, just remove this role.
5715                      role_unassign($withroleid, $ue->userid, $context->id);
5716  
5717                      unset($usersroles[$ue->userid][$withroleid]);
5718                  } else {
5719                      // If we remove all roles or user has only one role, unenrol user from course.
5720                      $plugin->unenrol_user($instance, $ue->userid);
5721                  }
5722                  $data->unenrolled[$ue->userid] = $ue->userid;
5723              }
5724              $rs->close();
5725          }
5726      }
5727      if (!empty($data->unenrolled)) {
5728          $status[] = array(
5729              'component' => $componentstr,
5730              'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5731              'error' => false
5732          );
5733      }
5734  
5735      $componentstr = get_string('groups');
5736  
5737      // Remove all group members.
5738      if (!empty($data->reset_groups_members)) {
5739          groups_delete_group_members($data->courseid);
5740          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5741      }
5742  
5743      // Remove all groups.
5744      if (!empty($data->reset_groups_remove)) {
5745          groups_delete_groups($data->courseid, false);
5746          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5747      }
5748  
5749      // Remove all grouping members.
5750      if (!empty($data->reset_groupings_members)) {
5751          groups_delete_groupings_groups($data->courseid, false);
5752          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5753      }
5754  
5755      // Remove all groupings.
5756      if (!empty($data->reset_groupings_remove)) {
5757          groups_delete_groupings($data->courseid, false);
5758          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5759      }
5760  
5761      // Look in every instance of every module for data to delete.
5762      $unsupportedmods = array();
5763      if ($allmods = $DB->get_records('modules') ) {
5764          foreach ($allmods as $mod) {
5765              $modname = $mod->name;
5766              $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5767              $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5768              if (file_exists($modfile)) {
5769                  if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5770                      continue; // Skip mods with no instances.
5771                  }
5772                  include_once($modfile);
5773                  if (function_exists($moddeleteuserdata)) {
5774                      $modstatus = $moddeleteuserdata($data);
5775                      if (is_array($modstatus)) {
5776                          $status = array_merge($status, $modstatus);
5777                      } else {
5778                          debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5779                      }
5780                  } else {
5781                      $unsupportedmods[] = $mod;
5782                  }
5783              } else {
5784                  debugging('Missing lib.php in '.$modname.' module!');
5785              }
5786              // Update calendar events for all modules.
5787              course_module_bulk_update_calendar_events($modname, $data->courseid);
5788          }
5789          // Purge the course cache after resetting course start date. MDL-76936
5790          if ($data->timeshift) {
5791              course_modinfo::purge_course_cache($data->courseid);
5792          }
5793      }
5794  
5795      // Mention unsupported mods.
5796      if (!empty($unsupportedmods)) {
5797          foreach ($unsupportedmods as $mod) {
5798              $status[] = array(
5799                  'component' => get_string('modulenameplural', $mod->name),
5800                  'item' => '',
5801                  'error' => get_string('resetnotimplemented')
5802              );
5803          }
5804      }
5805  
5806      $componentstr = get_string('gradebook', 'grades');
5807      // Reset gradebook,.
5808      if (!empty($data->reset_gradebook_items)) {
5809          remove_course_grades($data->courseid, false);
5810          grade_grab_course_grades($data->courseid);
5811          grade_regrade_final_grades($data->courseid);
5812          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5813  
5814      } else if (!empty($data->reset_gradebook_grades)) {
5815          grade_course_reset($data->courseid);
5816          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5817      }
5818      // Reset comments.
5819      if (!empty($data->reset_comments)) {
5820          require_once($CFG->dirroot.'/comment/lib.php');
5821          comment::reset_course_page_comments($context);
5822      }
5823  
5824      $event = \core\event\course_reset_ended::create($eventparams);
5825      $event->trigger();
5826  
5827      return $status;
5828  }
5829  
5830  /**
5831   * Generate an email processing address.
5832   *
5833   * @param int $modid
5834   * @param string $modargs
5835   * @return string Returns email processing address
5836   */
5837  function generate_email_processing_address($modid, $modargs) {
5838      global $CFG;
5839  
5840      $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5841      return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5842  }
5843  
5844  /**
5845   * ?
5846   *
5847   * @todo Finish documenting this function
5848   *
5849   * @param string $modargs
5850   * @param string $body Currently unused
5851   */
5852  function moodle_process_email($modargs, $body) {
5853      global $DB;
5854  
5855      // The first char should be an unencoded letter. We'll take this as an action.
5856      switch ($modargs[0]) {
5857          case 'B': { // Bounce.
5858              list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5859              if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5860                  // Check the half md5 of their email.
5861                  $md5check = substr(md5($user->email), 0, 16);
5862                  if ($md5check == substr($modargs, -16)) {
5863                      set_bounce_count($user);
5864                  }
5865                  // Else maybe they've already changed it?
5866              }
5867          }
5868          break;
5869          // Maybe more later?
5870      }
5871  }
5872  
5873  // CORRESPONDENCE.
5874  
5875  /**
5876   * Get mailer instance, enable buffering, flush buffer or disable buffering.
5877   *
5878   * @param string $action 'get', 'buffer', 'close' or 'flush'
5879   * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5880   */
5881  function get_mailer($action='get') {
5882      global $CFG;
5883  
5884      /** @var moodle_phpmailer $mailer */
5885      static $mailer  = null;
5886      static $counter = 0;
5887  
5888      if (!isset($CFG->smtpmaxbulk)) {
5889          $CFG->smtpmaxbulk = 1;
5890      }
5891  
5892      if ($action == 'get') {
5893          $prevkeepalive = false;
5894  
5895          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5896              if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5897                  $counter++;
5898                  // Reset the mailer.
5899                  $mailer->Priority         = 3;
5900                  $mailer->CharSet          = 'UTF-8'; // Our default.
5901                  $mailer->ContentType      = "text/plain";
5902                  $mailer->Encoding         = "8bit";
5903                  $mailer->From             = "root@localhost";
5904                  $mailer->FromName         = "Root User";
5905                  $mailer->Sender           = "";
5906                  $mailer->Subject          = "";
5907                  $mailer->Body             = "";
5908                  $mailer->AltBody          = "";
5909                  $mailer->ConfirmReadingTo = "";
5910  
5911                  $mailer->clearAllRecipients();
5912                  $mailer->clearReplyTos();
5913                  $mailer->clearAttachments();
5914                  $mailer->clearCustomHeaders();
5915                  return $mailer;
5916              }
5917  
5918              $prevkeepalive = $mailer->SMTPKeepAlive;
5919              get_mailer('flush');
5920          }
5921  
5922          require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5923          $mailer = new moodle_phpmailer();
5924  
5925          $counter = 1;
5926  
5927          if ($CFG->smtphosts == 'qmail') {
5928              // Use Qmail system.
5929              $mailer->isQmail();
5930  
5931          } else if (empty($CFG->smtphosts)) {
5932              // Use PHP mail() = sendmail.
5933              $mailer->isMail();
5934  
5935          } else {
5936              // Use SMTP directly.
5937              $mailer->isSMTP();
5938              if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5939                  $mailer->SMTPDebug = 3;
5940              }
5941              // Specify main and backup servers.
5942              $mailer->Host          = $CFG->smtphosts;
5943              // Specify secure connection protocol.
5944              $mailer->SMTPSecure    = $CFG->smtpsecure;
5945              // Use previous keepalive.
5946              $mailer->SMTPKeepAlive = $prevkeepalive;
5947  
5948              if ($CFG->smtpuser) {
5949                  // Use SMTP authentication.
5950                  $mailer->SMTPAuth = true;
5951                  $mailer->Username = $CFG->smtpuser;
5952                  $mailer->Password = $CFG->smtppass;
5953              }
5954          }
5955  
5956          return $mailer;
5957      }
5958  
5959      $nothing = null;
5960  
5961      // Keep smtp session open after sending.
5962      if ($action == 'buffer') {
5963          if (!empty($CFG->smtpmaxbulk)) {
5964              get_mailer('flush');
5965              $m = get_mailer();
5966              if ($m->Mailer == 'smtp') {
5967                  $m->SMTPKeepAlive = true;
5968              }
5969          }
5970          return $nothing;
5971      }
5972  
5973      // Close smtp session, but continue buffering.
5974      if ($action == 'flush') {
5975          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5976              if (!empty($mailer->SMTPDebug)) {
5977                  echo '<pre>'."\n";
5978              }
5979              $mailer->SmtpClose();
5980              if (!empty($mailer->SMTPDebug)) {
5981                  echo '</pre>';
5982              }
5983          }
5984          return $nothing;
5985      }
5986  
5987      // Close smtp session, do not buffer anymore.
5988      if ($action == 'close') {
5989          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5990              get_mailer('flush');
5991              $mailer->SMTPKeepAlive = false;
5992          }
5993          $mailer = null; // Better force new instance.
5994          return $nothing;
5995      }
5996  }
5997  
5998  /**
5999   * A helper function to test for email diversion
6000   *
6001   * @param string $email
6002   * @return bool Returns true if the email should be diverted
6003   */
6004  function email_should_be_diverted($email) {
6005      global $CFG;
6006  
6007      if (empty($CFG->divertallemailsto)) {
6008          return false;
6009      }
6010  
6011      if (empty($CFG->divertallemailsexcept)) {
6012          return true;
6013      }
6014  
6015      $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
6016      foreach ($patterns as $pattern) {
6017          if (preg_match("/{$pattern}/i", $email)) {
6018              return false;
6019          }
6020      }
6021  
6022      return true;
6023  }
6024  
6025  /**
6026   * Generate a unique email Message-ID using the moodle domain and install path
6027   *
6028   * @param string $localpart An optional unique message id prefix.
6029   * @return string The formatted ID ready for appending to the email headers.
6030   */
6031  function generate_email_messageid($localpart = null) {
6032      global $CFG;
6033  
6034      $urlinfo = parse_url($CFG->wwwroot);
6035      $base = '@' . $urlinfo['host'];
6036  
6037      // If multiple moodles are on the same domain we want to tell them
6038      // apart so we add the install path to the local part. This means
6039      // that the id local part should never contain a / character so
6040      // we can correctly parse the id to reassemble the wwwroot.
6041      if (isset($urlinfo['path'])) {
6042          $base = $urlinfo['path'] . $base;
6043      }
6044  
6045      if (empty($localpart)) {
6046          $localpart = uniqid('', true);
6047      }
6048  
6049      // Because we may have an option /installpath suffix to the local part
6050      // of the id we need to escape any / chars which are in the $localpart.
6051      $localpart = str_replace('/', '%2F', $localpart);
6052  
6053      return '<' . $localpart . $base . '>';
6054  }
6055  
6056  /**
6057   * Send an email to a specified user
6058   *
6059   * @param stdClass $user  A {@link $USER} object
6060   * @param stdClass $from A {@link $USER} object
6061   * @param string $subject plain text subject line of the email
6062   * @param string $messagetext plain text version of the message
6063   * @param string $messagehtml complete html version of the message (optional)
6064   * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
6065   *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
6066   * @param string $attachname the name of the file (extension indicates MIME)
6067   * @param bool $usetrueaddress determines whether $from email address should
6068   *          be sent out. Will be overruled by user profile setting for maildisplay
6069   * @param string $replyto Email address to reply to
6070   * @param string $replytoname Name of reply to recipient
6071   * @param int $wordwrapwidth custom word wrap width, default 79
6072   * @return bool Returns true if mail was sent OK and false if there was an error.
6073   */
6074  function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
6075                         $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
6076  
6077      global $CFG, $PAGE, $SITE;
6078  
6079      if (empty($user) or empty($user->id)) {
6080          debugging('Can not send email to null user', DEBUG_DEVELOPER);
6081          return false;
6082      }
6083  
6084      if (empty($user->email)) {
6085          debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
6086          return false;
6087      }
6088  
6089      if (!empty($user->deleted)) {
6090          debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
6091          return false;
6092      }
6093  
6094      if (defined('BEHAT_SITE_RUNNING')) {
6095          // Fake email sending in behat.
6096          return true;
6097      }
6098  
6099      if (!empty($CFG->noemailever)) {
6100          // Hidden setting for development sites, set in config.php if needed.
6101          debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
6102          return true;
6103      }
6104  
6105      if (email_should_be_diverted($user->email)) {
6106          $subject = "[DIVERTED {$user->email}] $subject";
6107          $user = clone($user);
6108          $user->email = $CFG->divertallemailsto;
6109      }
6110  
6111      // Skip mail to suspended users.
6112      if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
6113          return true;
6114      }
6115  
6116      if (!validate_email($user->email)) {
6117          // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6118          debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6119          return false;
6120      }
6121  
6122      if (over_bounce_threshold($user)) {
6123          debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6124          return false;
6125      }
6126  
6127      // TLD .invalid  is specifically reserved for invalid domain names.
6128      // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6129      if (substr($user->email, -8) == '.invalid') {
6130          debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6131          return true; // This is not an error.
6132      }
6133  
6134      // If the user is a remote mnet user, parse the email text for URL to the
6135      // wwwroot and modify the url to direct the user's browser to login at their
6136      // home site (identity provider - idp) before hitting the link itself.
6137      if (is_mnet_remote_user($user)) {
6138          require_once($CFG->dirroot.'/mnet/lib.php');
6139  
6140          $jumpurl = mnet_get_idp_jump_url($user);
6141          $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6142  
6143          $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6144                  $callback,
6145                  $messagetext);
6146          $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6147                  $callback,
6148                  $messagehtml);
6149      }
6150      $mail = get_mailer();
6151  
6152      if (!empty($mail->SMTPDebug)) {
6153          echo '<pre>' . "\n";
6154      }
6155  
6156      $temprecipients = array();
6157      $tempreplyto = array();
6158  
6159      // Make sure that we fall back onto some reasonable no-reply address.
6160      $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6161      $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6162  
6163      if (!validate_email($noreplyaddress)) {
6164          debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6165          $noreplyaddress = $noreplyaddressdefault;
6166      }
6167  
6168      // Make up an email address for handling bounces.
6169      if (!empty($CFG->handlebounces)) {
6170          $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6171          $mail->Sender = generate_email_processing_address(0, $modargs);
6172      } else {
6173          $mail->Sender = $noreplyaddress;
6174      }
6175  
6176      // Make sure that the explicit replyto is valid, fall back to the implicit one.
6177      if (!empty($replyto) && !validate_email($replyto)) {
6178          debugging('email_to_user: Invalid replyto-email '.s($replyto));
6179          $replyto = $noreplyaddress;
6180      }
6181  
6182      if (is_string($from)) { // So we can pass whatever we want if there is need.
6183          $mail->From     = $noreplyaddress;
6184          $mail->FromName = $from;
6185      // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6186      // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6187      // in a course with the sender.
6188      } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6189          if (!validate_email($from->email)) {
6190              debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6191              // Better not to use $noreplyaddress in this case.
6192              return false;
6193          }
6194          $mail->From = $from->email;
6195          $fromdetails = new stdClass();
6196          $fromdetails->name = fullname($from);
6197          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6198          $fromdetails->siteshortname = format_string($SITE->shortname);
6199          $fromstring = $fromdetails->name;
6200          if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6201              $fromstring = get_string('emailvia', 'core', $fromdetails);
6202          }
6203          $mail->FromName = $fromstring;
6204          if (empty($replyto)) {
6205              $tempreplyto[] = array($from->email, fullname($from));
6206          }
6207      } else {
6208          $mail->From = $noreplyaddress;
6209          $fromdetails = new stdClass();
6210          $fromdetails->name = fullname($from);
6211          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6212          $fromdetails->siteshortname = format_string($SITE->shortname);
6213          $fromstring = $fromdetails->name;
6214          if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6215              $fromstring = get_string('emailvia', 'core', $fromdetails);
6216          }
6217          $mail->FromName = $fromstring;
6218          if (empty($replyto)) {
6219              $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6220          }
6221      }
6222  
6223      if (!empty($replyto)) {
6224          $tempreplyto[] = array($replyto, $replytoname);
6225      }
6226  
6227      $temprecipients[] = array($user->email, fullname($user));
6228  
6229      // Set word wrap.
6230      $mail->WordWrap = $wordwrapwidth;
6231  
6232      if (!empty($from->customheaders)) {
6233          // Add custom headers.
6234          if (is_array($from->customheaders)) {
6235              foreach ($from->customheaders as $customheader) {
6236                  $mail->addCustomHeader($customheader);
6237              }
6238          } else {
6239              $mail->addCustomHeader($from->customheaders);
6240          }
6241      }
6242  
6243      // If the X-PHP-Originating-Script email header is on then also add an additional
6244      // header with details of where exactly in moodle the email was triggered from,
6245      // either a call to message_send() or to email_to_user().
6246      if (ini_get('mail.add_x_header')) {
6247  
6248          $stack = debug_backtrace(false);
6249          $origin = $stack[0];
6250  
6251          foreach ($stack as $depth => $call) {
6252              if ($call['function'] == 'message_send') {
6253                  $origin = $call;
6254              }
6255          }
6256  
6257          $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6258               . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6259          $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6260      }
6261  
6262      if (!empty($CFG->emailheaders)) {
6263          $headers = array_map('trim', explode("\n", $CFG->emailheaders));
6264          foreach ($headers as $header) {
6265              if (!empty($header)) {
6266                  $mail->addCustomHeader($header);
6267              }
6268          }
6269      }
6270  
6271      if (!empty($from->priority)) {
6272          $mail->Priority = $from->priority;
6273      }
6274  
6275      $renderer = $PAGE->get_renderer('core');
6276      $context = array(
6277          'sitefullname' => $SITE->fullname,
6278          'siteshortname' => $SITE->shortname,
6279          'sitewwwroot' => $CFG->wwwroot,
6280          'subject' => $subject,
6281          'prefix' => $CFG->emailsubjectprefix,
6282          'to' => $user->email,
6283          'toname' => fullname($user),
6284          'from' => $mail->From,
6285          'fromname' => $mail->FromName,
6286      );
6287      if (!empty($tempreplyto[0])) {
6288          $context['replyto'] = $tempreplyto[0][0];
6289          $context['replytoname'] = $tempreplyto[0][1];
6290      }
6291      if ($user->id > 0) {
6292          $context['touserid'] = $user->id;
6293          $context['tousername'] = $user->username;
6294      }
6295  
6296      if (!empty($user->mailformat) && $user->mailformat == 1) {
6297          // Only process html templates if the user preferences allow html email.
6298  
6299          if (!$messagehtml) {
6300              // If no html has been given, BUT there is an html wrapping template then
6301              // auto convert the text to html and then wrap it.
6302              $messagehtml = trim(text_to_html($messagetext));
6303          }
6304          $context['body'] = $messagehtml;
6305          $messagehtml = $renderer->render_from_template('core/email_html', $context);
6306      }
6307  
6308      $context['body'] = html_to_text(nl2br($messagetext));
6309      $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6310      $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6311      $messagetext = $renderer->render_from_template('core/email_text', $context);
6312  
6313      // Autogenerate a MessageID if it's missing.
6314      if (empty($mail->MessageID)) {
6315          $mail->MessageID = generate_email_messageid();
6316      }
6317  
6318      if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6319          // Don't ever send HTML to users who don't want it.
6320          $mail->isHTML(true);
6321          $mail->Encoding = 'quoted-printable';
6322          $mail->Body    =  $messagehtml;
6323          $mail->AltBody =  "\n$messagetext\n";
6324      } else {
6325          $mail->IsHTML(false);
6326          $mail->Body =  "\n$messagetext\n";
6327      }
6328  
6329      if ($attachment && $attachname) {
6330          if (preg_match( "~\\.\\.~" , $attachment )) {
6331              // Security check for ".." in dir path.
6332              $supportuser = core_user::get_support_user();
6333              $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6334              $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6335          } else {
6336              require_once($CFG->libdir.'/filelib.php');
6337              $mimetype = mimeinfo('type', $attachname);
6338  
6339              // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6340              // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6341              $attachpath = str_replace('\\', '/', realpath($attachment));
6342  
6343              // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6344              $allowedpaths = array_map(function(string $path): string {
6345                  return str_replace('\\', '/', realpath($path));
6346              }, [
6347                  $CFG->cachedir,
6348                  $CFG->dataroot,
6349                  $CFG->dirroot,
6350                  $CFG->localcachedir,
6351                  $CFG->tempdir,
6352                  $CFG->localrequestdir,
6353              ]);
6354  
6355              // Set addpath to true.
6356              $addpath = true;
6357  
6358              // Check if attachment includes one of the allowed paths.
6359              foreach (array_filter($allowedpaths) as $allowedpath) {
6360                  // Set addpath to false if the attachment includes one of the allowed paths.
6361                  if (strpos($attachpath, $allowedpath) === 0) {
6362                      $addpath = false;
6363                      break;
6364                  }
6365              }
6366  
6367              // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6368              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6369              if ($addpath == true) {
6370                  $attachment = $CFG->dataroot . '/' . $attachment;
6371              }
6372  
6373              $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6374          }
6375      }
6376  
6377      // Check if the email should be sent in an other charset then the default UTF-8.
6378      if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6379  
6380          // Use the defined site mail charset or eventually the one preferred by the recipient.
6381          $charset = $CFG->sitemailcharset;
6382          if (!empty($CFG->allowusermailcharset)) {
6383              if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6384                  $charset = $useremailcharset;
6385              }
6386          }
6387  
6388          // Convert all the necessary strings if the charset is supported.
6389          $charsets = get_list_of_charsets();
6390          unset($charsets['UTF-8']);
6391          if (in_array($charset, $charsets)) {
6392              $mail->CharSet  = $charset;
6393              $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6394              $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6395              $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6396              $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6397  
6398              foreach ($temprecipients as $key => $values) {
6399                  $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6400              }
6401              foreach ($tempreplyto as $key => $values) {
6402                  $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6403              }
6404          }
6405      }
6406  
6407      foreach ($temprecipients as $values) {
6408          $mail->addAddress($values[0], $values[1]);
6409      }
6410      foreach ($tempreplyto as $values) {
6411          $mail->addReplyTo($values[0], $values[1]);
6412      }
6413  
6414      if (!empty($CFG->emaildkimselector)) {
6415          $domain = substr(strrchr($mail->From, "@"), 1);
6416          $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6417          if (file_exists($pempath)) {
6418              $mail->DKIM_domain      = $domain;
6419              $mail->DKIM_private     = $pempath;
6420              $mail->DKIM_selector    = $CFG->emaildkimselector;
6421              $mail->DKIM_identity    = $mail->From;
6422          } else {
6423              debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6424          }
6425      }
6426  
6427      if ($mail->send()) {
6428          set_send_count($user);
6429          if (!empty($mail->SMTPDebug)) {
6430              echo '</pre>';
6431          }
6432          return true;
6433      } else {
6434          // Trigger event for failing to send email.
6435          $event = \core\event\email_failed::create(array(
6436              'context' => context_system::instance(),
6437              'userid' => $from->id,
6438              'relateduserid' => $user->id,
6439              'other' => array(
6440                  'subject' => $subject,
6441                  'message' => $messagetext,
6442                  'errorinfo' => $mail->ErrorInfo
6443              )
6444          ));
6445          $event->trigger();
6446          if (CLI_SCRIPT) {
6447              mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6448          }
6449          if (!empty($mail->SMTPDebug)) {
6450              echo '</pre>';
6451          }
6452          return false;
6453      }
6454  }
6455  
6456  /**
6457   * Check to see if a user's real email address should be used for the "From" field.
6458   *
6459   * @param  object $from The user object for the user we are sending the email from.
6460   * @param  object $user The user object that we are sending the email to.
6461   * @param  array $unused No longer used.
6462   * @return bool Returns true if we can use the from user's email adress in the "From" field.
6463   */
6464  function can_send_from_real_email_address($from, $user, $unused = null) {
6465      global $CFG;
6466      if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6467          return false;
6468      }
6469      $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6470      // Email is in the list of allowed domains for sending email,
6471      // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6472      // in a course with the sender.
6473      if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6474                  && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6475                  || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6476                  && enrol_get_shared_courses($user, $from, false, true)))) {
6477          return true;
6478      }
6479      return false;
6480  }
6481  
6482  /**
6483   * Generate a signoff for emails based on support settings
6484   *
6485   * @return string
6486   */
6487  function generate_email_signoff() {
6488      global $CFG, $OUTPUT;
6489  
6490      $signoff = "\n";
6491      if (!empty($CFG->supportname)) {
6492          $signoff .= $CFG->supportname."\n";
6493      }
6494  
6495      $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6496  
6497      if ($supportemail) {
6498          $signoff .= "\n" . $supportemail . "\n";
6499      }
6500  
6501      return $signoff;
6502  }
6503  
6504  /**
6505   * Sets specified user's password and send the new password to the user via email.
6506   *
6507   * @param stdClass $user A {@link $USER} object
6508   * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6509   * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6510   */
6511  function setnew_password_and_mail($user, $fasthash = false) {
6512      global $CFG, $DB;
6513  
6514      // We try to send the mail in language the user understands,
6515      // unfortunately the filter_string() does not support alternative langs yet
6516      // so multilang will not work properly for site->fullname.
6517      $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6518  
6519      $site  = get_site();
6520  
6521      $supportuser = core_user::get_support_user();
6522  
6523      $newpassword = generate_password();
6524  
6525      update_internal_user_password($user, $newpassword, $fasthash);
6526  
6527      $a = new stdClass();
6528      $a->firstname   = fullname($user, true);
6529      $a->sitename    = format_string($site->fullname);
6530      $a->username    = $user->username;
6531      $a->newpassword = $newpassword;
6532      $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6533      $a->signoff     = generate_email_signoff();
6534  
6535      $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6536  
6537      $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6538  
6539      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6540      return email_to_user($user, $supportuser, $subject, $message);
6541  
6542  }
6543  
6544  /**
6545   * Resets specified user's password and send the new password to the user via email.
6546   *
6547   * @param stdClass $user A {@link $USER} object
6548   * @return bool Returns true if mail was sent OK and false if there was an error.
6549   */
6550  function reset_password_and_mail($user) {
6551      global $CFG;
6552  
6553      $site  = get_site();
6554      $supportuser = core_user::get_support_user();
6555  
6556      $userauth = get_auth_plugin($user->auth);
6557      if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6558          trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6559          return false;
6560      }
6561  
6562      $newpassword = generate_password();
6563  
6564      if (!$userauth->user_update_password($user, $newpassword)) {
6565          throw new \moodle_exception("cannotsetpassword");
6566      }
6567  
6568      $a = new stdClass();
6569      $a->firstname   = $user->firstname;
6570      $a->lastname    = $user->lastname;
6571      $a->sitename    = format_string($site->fullname);
6572      $a->username    = $user->username;
6573      $a->newpassword = $newpassword;
6574      $a->link        = $CFG->wwwroot .'/login/change_password.php';
6575      $a->signoff     = generate_email_signoff();
6576  
6577      $message = get_string('newpasswordtext', '', $a);
6578  
6579      $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6580  
6581      unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6582  
6583      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6584      return email_to_user($user, $supportuser, $subject, $message);
6585  }
6586  
6587  /**
6588   * Send email to specified user with confirmation text and activation link.
6589   *
6590   * @param stdClass $user A {@link $USER} object
6591   * @param string $confirmationurl user confirmation URL
6592   * @return bool Returns true if mail was sent OK and false if there was an error.
6593   */
6594  function send_confirmation_email($user, $confirmationurl = null) {
6595      global $CFG;
6596  
6597      $site = get_site();
6598      $supportuser = core_user::get_support_user();
6599  
6600      $data = new stdClass();
6601      $data->sitename  = format_string($site->fullname);
6602      $data->admin     = generate_email_signoff();
6603  
6604      $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6605  
6606      if (empty($confirmationurl)) {
6607          $confirmationurl = '/login/confirm.php';
6608      }
6609  
6610      $confirmationurl = new moodle_url($confirmationurl);
6611      // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6612      $confirmationurl->remove_params('data');
6613      $confirmationpath = $confirmationurl->out(false);
6614  
6615      // We need to custom encode the username to include trailing dots in the link.
6616      // Because of this custom encoding we can't use moodle_url directly.
6617      // Determine if a query string is present in the confirmation url.
6618      $hasquerystring = strpos($confirmationpath, '?') !== false;
6619      // Perform normal url encoding of the username first.
6620      $username = urlencode($user->username);
6621      // Prevent problems with trailing dots not being included as part of link in some mail clients.
6622      $username = str_replace('.', '%2E', $username);
6623  
6624      $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6625  
6626      $message     = get_string('emailconfirmation', '', $data);
6627      $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6628  
6629      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6630      return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6631  }
6632  
6633  /**
6634   * Sends a password change confirmation email.
6635   *
6636   * @param stdClass $user A {@link $USER} object
6637   * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6638   * @return bool Returns true if mail was sent OK and false if there was an error.
6639   */
6640  function send_password_change_confirmation_email($user, $resetrecord) {
6641      global $CFG;
6642  
6643      $site = get_site();
6644      $supportuser = core_user::get_support_user();
6645      $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6646  
6647      $data = new stdClass();
6648      $data->firstname = $user->firstname;
6649      $data->lastname  = $user->lastname;
6650      $data->username  = $user->username;
6651      $data->sitename  = format_string($site->fullname);
6652      $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6653      $data->admin     = generate_email_signoff();
6654      $data->resetminutes = $pwresetmins;
6655  
6656      $message = get_string('emailresetconfirmation', '', $data);
6657      $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6658  
6659      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6660      return email_to_user($user, $supportuser, $subject, $message);
6661  
6662  }
6663  
6664  /**
6665   * Sends an email containing information on how to change your password.
6666   *
6667   * @param stdClass $user A {@link $USER} object
6668   * @return bool Returns true if mail was sent OK and false if there was an error.
6669   */
6670  function send_password_change_info($user) {
6671      $site = get_site();
6672      $supportuser = core_user::get_support_user();
6673  
6674      $data = new stdClass();
6675      $data->firstname = $user->firstname;
6676      $data->lastname  = $user->lastname;
6677      $data->username  = $user->username;
6678      $data->sitename  = format_string($site->fullname);
6679      $data->admin     = generate_email_signoff();
6680  
6681      if (!is_enabled_auth($user->auth)) {
6682          $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6683          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6684          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6685          return email_to_user($user, $supportuser, $subject, $message);
6686      }
6687  
6688      $userauth = get_auth_plugin($user->auth);
6689      ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6690  
6691      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6692      return email_to_user($user, $supportuser, $subject, $message);
6693  }
6694  
6695  /**
6696   * Check that an email is allowed.  It returns an error message if there was a problem.
6697   *
6698   * @param string $email Content of email
6699   * @return string|false
6700   */
6701  function email_is_not_allowed($email) {
6702      global $CFG;
6703  
6704      // Comparing lowercase domains.
6705      $email = strtolower($email);
6706      if (!empty($CFG->allowemailaddresses)) {
6707          $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6708          foreach ($allowed as $allowedpattern) {
6709              $allowedpattern = trim($allowedpattern);
6710              if (!$allowedpattern) {
6711                  continue;
6712              }
6713              if (strpos($allowedpattern, '.') === 0) {
6714                  if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6715                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6716                      return false;
6717                  }
6718  
6719              } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6720                  return false;
6721              }
6722          }
6723          return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6724  
6725      } else if (!empty($CFG->denyemailaddresses)) {
6726          $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6727          foreach ($denied as $deniedpattern) {
6728              $deniedpattern = trim($deniedpattern);
6729              if (!$deniedpattern) {
6730                  continue;
6731              }
6732              if (strpos($deniedpattern, '.') === 0) {
6733                  if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6734                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6735                      return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6736                  }
6737  
6738              } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6739                  return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6740              }
6741          }
6742      }
6743  
6744      return false;
6745  }
6746  
6747  // FILE HANDLING.
6748  
6749  /**
6750   * Returns local file storage instance
6751   *
6752   * @return file_storage
6753   */
6754  function get_file_storage($reset = false) {
6755      global $CFG;
6756  
6757      static $fs = null;
6758  
6759      if ($reset) {
6760          $fs = null;
6761          return;
6762      }
6763  
6764      if ($fs) {
6765          return $fs;
6766      }
6767  
6768      require_once("$CFG->libdir/filelib.php");
6769  
6770      $fs = new file_storage();
6771  
6772      return $fs;
6773  }
6774  
6775  /**
6776   * Returns local file storage instance
6777   *
6778   * @return file_browser
6779   */
6780  function get_file_browser() {
6781      global $CFG;
6782  
6783      static $fb = null;
6784  
6785      if ($fb) {
6786          return $fb;
6787      }
6788  
6789      require_once("$CFG->libdir/filelib.php");
6790  
6791      $fb = new file_browser();
6792  
6793      return $fb;
6794  }
6795  
6796  /**
6797   * Returns file packer
6798   *
6799   * @param string $mimetype default application/zip
6800   * @return file_packer
6801   */
6802  function get_file_packer($mimetype='application/zip') {
6803      global $CFG;
6804  
6805      static $fp = array();
6806  
6807      if (isset($fp[$mimetype])) {
6808          return $fp[$mimetype];
6809      }
6810  
6811      switch ($mimetype) {
6812          case 'application/zip':
6813          case 'application/vnd.moodle.profiling':
6814              $classname = 'zip_packer';
6815              break;
6816  
6817          case 'application/x-gzip' :
6818              $classname = 'tgz_packer';
6819              break;
6820  
6821          case 'application/vnd.moodle.backup':
6822              $classname = 'mbz_packer';
6823              break;
6824  
6825          default:
6826              return false;
6827      }
6828  
6829      require_once("$CFG->libdir/filestorage/$classname.php");
6830      $fp[$mimetype] = new $classname();
6831  
6832      return $fp[$mimetype];
6833  }
6834  
6835  /**
6836   * Returns current name of file on disk if it exists.
6837   *
6838   * @param string $newfile File to be verified
6839   * @return string Current name of file on disk if true
6840   */
6841  function valid_uploaded_file($newfile) {
6842      if (empty($newfile)) {
6843          return '';
6844      }
6845      if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6846          return $newfile['tmp_name'];
6847      } else {
6848          return '';
6849      }
6850  }
6851  
6852  /**
6853   * Returns the maximum size for uploading files.
6854   *
6855   * There are seven possible upload limits:
6856   * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6857   * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6858   * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6859   * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6860   * 5. by the Moodle admin in $CFG->maxbytes
6861   * 6. by the teacher in the current course $course->maxbytes
6862   * 7. by the teacher for the current module, eg $assignment->maxbytes
6863   *
6864   * These last two are passed to this function as arguments (in bytes).
6865   * Anything defined as 0 is ignored.
6866   * The smallest of all the non-zero numbers is returned.
6867   *
6868   * @todo Finish documenting this function
6869   *
6870   * @param int $sitebytes Set maximum size
6871   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6872   * @param int $modulebytes Current module ->maxbytes (in bytes)
6873   * @param bool $unused This parameter has been deprecated and is not used any more.
6874   * @return int The maximum size for uploading files.
6875   */
6876  function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6877  
6878      if (! $filesize = ini_get('upload_max_filesize')) {
6879          $filesize = '5M';
6880      }
6881      $minimumsize = get_real_size($filesize);
6882  
6883      if ($postsize = ini_get('post_max_size')) {
6884          $postsize = get_real_size($postsize);
6885          if ($postsize < $minimumsize) {
6886              $minimumsize = $postsize;
6887          }
6888      }
6889  
6890      if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6891          $minimumsize = $sitebytes;
6892      }
6893  
6894      if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6895          $minimumsize = $coursebytes;
6896      }
6897  
6898      if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6899          $minimumsize = $modulebytes;
6900      }
6901  
6902      return $minimumsize;
6903  }
6904  
6905  /**
6906   * Returns the maximum size for uploading files for the current user
6907   *
6908   * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6909   *
6910   * @param context $context The context in which to check user capabilities
6911   * @param int $sitebytes Set maximum size
6912   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6913   * @param int $modulebytes Current module ->maxbytes (in bytes)
6914   * @param stdClass $user The user
6915   * @param bool $unused This parameter has been deprecated and is not used any more.
6916   * @return int The maximum size for uploading files.
6917   */
6918  function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6919          $unused = false) {
6920      global $USER;
6921  
6922      if (empty($user)) {
6923          $user = $USER;
6924      }
6925  
6926      if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6927          return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6928      }
6929  
6930      return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6931  }
6932  
6933  /**
6934   * Returns an array of possible sizes in local language
6935   *
6936   * Related to {@link get_max_upload_file_size()} - this function returns an
6937   * array of possible sizes in an array, translated to the
6938   * local language.
6939   *
6940   * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6941   *
6942   * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6943   * with the value set to 0. This option will be the first in the list.
6944   *
6945   * @uses SORT_NUMERIC
6946   * @param int $sitebytes Set maximum size
6947   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6948   * @param int $modulebytes Current module ->maxbytes (in bytes)
6949   * @param int|array $custombytes custom upload size/s which will be added to list,
6950   *        Only value/s smaller then maxsize will be added to list.
6951   * @return array
6952   */
6953  function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6954      global $CFG;
6955  
6956      if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6957          return array();
6958      }
6959  
6960      if ($sitebytes == 0) {
6961          // Will get the minimum of upload_max_filesize or post_max_size.
6962          $sitebytes = get_max_upload_file_size();
6963      }
6964  
6965      $filesize = array();
6966      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6967                        5242880, 10485760, 20971520, 52428800, 104857600,
6968                        262144000, 524288000, 786432000, 1073741824,
6969                        2147483648, 4294967296, 8589934592);
6970  
6971      // If custombytes is given and is valid then add it to the list.
6972      if (is_number($custombytes) and $custombytes > 0) {
6973          $custombytes = (int)$custombytes;
6974          if (!in_array($custombytes, $sizelist)) {
6975              $sizelist[] = $custombytes;
6976          }
6977      } else if (is_array($custombytes)) {
6978          $sizelist = array_unique(array_merge($sizelist, $custombytes));
6979      }
6980  
6981      // Allow maxbytes to be selected if it falls outside the above boundaries.
6982      if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6983          // Note: get_real_size() is used in order to prevent problems with invalid values.
6984          $sizelist[] = get_real_size($CFG->maxbytes);
6985      }
6986  
6987      foreach ($sizelist as $sizebytes) {
6988          if ($sizebytes < $maxsize && $sizebytes > 0) {
6989              $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6990          }
6991      }
6992  
6993      $limitlevel = '';
6994      $displaysize = '';
6995      if ($modulebytes &&
6996          (($modulebytes < $coursebytes || $coursebytes == 0) &&
6997           ($modulebytes < $sitebytes || $sitebytes == 0))) {
6998          $limitlevel = get_string('activity', 'core');
6999          $displaysize = display_size($modulebytes, 0);
7000          $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
7001  
7002      } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
7003          $limitlevel = get_string('course', 'core');
7004          $displaysize = display_size($coursebytes, 0);
7005          $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
7006  
7007      } else if ($sitebytes) {
7008          $limitlevel = get_string('site', 'core');
7009          $displaysize = display_size($sitebytes, 0);
7010          $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
7011      }
7012  
7013      krsort($filesize, SORT_NUMERIC);
7014      if ($limitlevel) {
7015          $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
7016          $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
7017      }
7018  
7019      return $filesize;
7020  }
7021  
7022  /**
7023   * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
7024   *
7025   * If excludefiles is defined, then that file/directory is ignored
7026   * If getdirs is true, then (sub)directories are included in the output
7027   * If getfiles is true, then files are included in the output
7028   * (at least one of these must be true!)
7029   *
7030   * @todo Finish documenting this function. Add examples of $excludefile usage.
7031   *
7032   * @param string $rootdir A given root directory to start from
7033   * @param string|array $excludefiles If defined then the specified file/directory is ignored
7034   * @param bool $descend If true then subdirectories are recursed as well
7035   * @param bool $getdirs If true then (sub)directories are included in the output
7036   * @param bool $getfiles  If true then files are included in the output
7037   * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
7038   */
7039  function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
7040  
7041      $dirs = array();
7042  
7043      if (!$getdirs and !$getfiles) {   // Nothing to show.
7044          return $dirs;
7045      }
7046  
7047      if (!is_dir($rootdir)) {          // Must be a directory.
7048          return $dirs;
7049      }
7050  
7051      if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
7052          return $dirs;
7053      }
7054  
7055      if (!is_array($excludefiles)) {
7056          $excludefiles = array($excludefiles);
7057      }
7058  
7059      while (false !== ($file = readdir($dir))) {
7060          $firstchar = substr($file, 0, 1);
7061          if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
7062              continue;
7063          }
7064          $fullfile = $rootdir .'/'. $file;
7065          if (filetype($fullfile) == 'dir') {
7066              if ($getdirs) {
7067                  $dirs[] = $file;
7068              }
7069              if ($descend) {
7070                  $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
7071                  foreach ($subdirs as $subdir) {
7072                      $dirs[] = $file .'/'. $subdir;
7073                  }
7074              }
7075          } else if ($getfiles) {
7076              $dirs[] = $file;
7077          }
7078      }
7079      closedir($dir);
7080  
7081      asort($dirs);
7082  
7083      return $dirs;
7084  }
7085  
7086  
7087  /**
7088   * Adds up all the files in a directory and works out the size.
7089   *
7090   * @param string $rootdir  The directory to start from
7091   * @param string $excludefile A file to exclude when summing directory size
7092   * @return int The summed size of all files and subfiles within the root directory
7093   */
7094  function get_directory_size($rootdir, $excludefile='') {
7095      global $CFG;
7096  
7097      // Do it this way if we can, it's much faster.
7098      if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
7099          $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
7100          $output = null;
7101          $return = null;
7102          exec($command, $output, $return);
7103          if (is_array($output)) {
7104              // We told it to return k.
7105              return get_real_size(intval($output[0]).'k');
7106          }
7107      }
7108  
7109      if (!is_dir($rootdir)) {
7110          // Must be a directory.
7111          return 0;
7112      }
7113  
7114      if (!$dir = @opendir($rootdir)) {
7115          // Can't open it for some reason.
7116          return 0;
7117      }
7118  
7119      $size = 0;
7120  
7121      while (false !== ($file = readdir($dir))) {
7122          $firstchar = substr($file, 0, 1);
7123          if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7124              continue;
7125          }
7126          $fullfile = $rootdir .'/'. $file;
7127          if (filetype($fullfile) == 'dir') {
7128              $size += get_directory_size($fullfile, $excludefile);
7129          } else {
7130              $size += filesize($fullfile);
7131          }
7132      }
7133      closedir($dir);
7134  
7135      return $size;
7136  }
7137  
7138  /**
7139   * Converts bytes into display form
7140   *
7141   * @param int $size  The size to convert to human readable form
7142   * @param int $decimalplaces If specified, uses fixed number of decimal places
7143   * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
7144   * @return string Display version of size
7145   */
7146  function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
7147  
7148      static $units;
7149  
7150      if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
7151          return get_string('unlimited');
7152      }
7153  
7154      if (empty($units)) {
7155          $units[] = get_string('sizeb');
7156          $units[] = get_string('sizekb');
7157          $units[] = get_string('sizemb');
7158          $units[] = get_string('sizegb');
7159          $units[] = get_string('sizetb');
7160          $units[] = get_string('sizepb');
7161      }
7162  
7163      switch ($fixedunits) {
7164          case 'PB' :
7165              $magnitude = 5;
7166              break;
7167          case 'TB' :
7168              $magnitude = 4;
7169              break;
7170          case 'GB' :
7171              $magnitude = 3;
7172              break;
7173          case 'MB' :
7174              $magnitude = 2;
7175              break;
7176          case 'KB' :
7177              $magnitude = 1;
7178              break;
7179          case 'B' :
7180              $magnitude = 0;
7181              break;
7182          case '':
7183              $magnitude = floor(log($size, 1024));
7184              $magnitude = max(0, min(5, $magnitude));
7185              break;
7186          default:
7187              throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
7188      }
7189  
7190      // Special case for magnitude 0 (bytes) - never use decimal places.
7191      $nbsp = "\xc2\xa0";
7192      if ($magnitude === 0) {
7193          return round($size) . $nbsp . $units[$magnitude];
7194      }
7195  
7196      // Convert to specified units.
7197      $sizeinunit = $size / 1024 ** $magnitude;
7198  
7199      // Fixed decimal places.
7200      return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
7201  }
7202  
7203  /**
7204   * Cleans a given filename by removing suspicious or troublesome characters
7205   *
7206   * @see clean_param()
7207   * @param string $string file name
7208   * @return string cleaned file name
7209   */
7210  function clean_filename($string) {
7211      return clean_param($string, PARAM_FILE);
7212  }
7213  
7214  // STRING TRANSLATION.
7215  
7216  /**
7217   * Returns the code for the current language
7218   *
7219   * @category string
7220   * @return string
7221   */
7222  function current_language() {
7223      global $CFG, $PAGE, $SESSION, $USER;
7224  
7225      if (!empty($SESSION->forcelang)) {
7226          // Allows overriding course-forced language (useful for admins to check
7227          // issues in courses whose language they don't understand).
7228          // Also used by some code to temporarily get language-related information in a
7229          // specific language (see force_current_language()).
7230          $return = $SESSION->forcelang;
7231  
7232      } else if (!empty($PAGE->cm->lang)) {
7233          // Activity language, if set.
7234          $return = $PAGE->cm->lang;
7235  
7236      } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
7237          // Course language can override all other settings for this page.
7238          $return = $PAGE->course->lang;
7239  
7240      } else if (!empty($SESSION->lang)) {
7241          // Session language can override other settings.
7242          $return = $SESSION->lang;
7243  
7244      } else if (!empty($USER->lang)) {
7245          $return = $USER->lang;
7246  
7247      } else if (isset($CFG->lang)) {
7248          $return = $CFG->lang;
7249  
7250      } else {
7251          $return = 'en';
7252      }
7253  
7254      // Just in case this slipped in from somewhere by accident.
7255      $return = str_replace('_utf8', '', $return);
7256  
7257      return $return;
7258  }
7259  
7260  /**
7261   * Fix the current language to the given language code.
7262   *
7263   * @param string $lang The language code to use.
7264   * @return void
7265   */
7266  function fix_current_language(string $lang): void {
7267      global $CFG, $COURSE, $SESSION, $USER;
7268  
7269      if (!get_string_manager()->translation_exists($lang)) {
7270          throw new coding_exception("The language pack for $lang is not available");
7271      }
7272  
7273      $fixglobal = '';
7274      $fixlang = 'lang';
7275      if (!empty($SESSION->forcelang)) {
7276          $fixglobal = $SESSION;
7277          $fixlang = 'forcelang';
7278      } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
7279          $fixglobal = $COURSE;
7280      } else if (!empty($SESSION->lang)) {
7281          $fixglobal = $SESSION;
7282      } else if (!empty($USER->lang)) {
7283          $fixglobal = $USER;
7284      } else if (isset($CFG->lang)) {
7285          set_config('lang', $lang);
7286      }
7287  
7288      if ($fixglobal) {
7289          $fixglobal->$fixlang = $lang;
7290      }
7291  }
7292  
7293  /**
7294   * Returns parent language of current active language if defined
7295   *
7296   * @category string
7297   * @param string $lang null means current language
7298   * @return string
7299   */
7300  function get_parent_language($lang=null) {
7301  
7302      $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7303  
7304      if ($parentlang === 'en') {
7305          $parentlang = '';
7306      }
7307  
7308      return $parentlang;
7309  }
7310  
7311  /**
7312   * Force the current language to get strings and dates localised in the given language.
7313   *
7314   * After calling this function, all strings will be provided in the given language
7315   * until this function is called again, or equivalent code is run.
7316   *
7317   * @param string $language
7318   * @return string previous $SESSION->forcelang value
7319   */
7320  function force_current_language($language) {
7321      global $SESSION;
7322      $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7323      if ($language !== $sessionforcelang) {
7324          // Setting forcelang to null or an empty string disables its effect.
7325          if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7326              $SESSION->forcelang = $language;
7327              moodle_setlocale();
7328          }
7329      }
7330      return $sessionforcelang;
7331  }
7332  
7333  /**
7334   * Returns current string_manager instance.
7335   *
7336   * The param $forcereload is needed for CLI installer only where the string_manager instance
7337   * must be replaced during the install.php script life time.
7338   *
7339   * @category string
7340   * @param bool $forcereload shall the singleton be released and new instance created instead?
7341   * @return core_string_manager
7342   */
7343  function get_string_manager($forcereload=false) {
7344      global $CFG;
7345  
7346      static $singleton = null;
7347  
7348      if ($forcereload) {
7349          $singleton = null;
7350      }
7351      if ($singleton === null) {
7352          if (empty($CFG->early_install_lang)) {
7353  
7354              $transaliases = array();
7355              if (empty($CFG->langlist)) {
7356                   $translist = array();
7357              } else {
7358                  $translist = explode(',', $CFG->langlist);
7359                  $translist = array_map('trim', $translist);
7360                  // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7361                  foreach ($translist as $i => $value) {
7362                      $parts = preg_split('/\s*\|\s*/', $value, 2);
7363                      if (count($parts) == 2) {
7364                          $transaliases[$parts[0]] = $parts[1];
7365                          $translist[$i] = $parts[0];
7366                      }
7367                  }
7368              }
7369  
7370              if (!empty($CFG->config_php_settings['customstringmanager'])) {
7371                  $classname = $CFG->config_php_settings['customstringmanager'];
7372  
7373                  if (class_exists($classname)) {
7374                      $implements = class_implements($classname);
7375  
7376                      if (isset($implements['core_string_manager'])) {
7377                          $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7378                          return $singleton;
7379  
7380                      } else {
7381                          debugging('Unable to instantiate custom string manager: class '.$classname.
7382                              ' does not implement the core_string_manager interface.');
7383                      }
7384  
7385                  } else {
7386                      debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7387                  }
7388              }
7389  
7390              $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7391  
7392          } else {
7393              $singleton = new core_string_manager_install();
7394          }
7395      }
7396  
7397      return $singleton;
7398  }
7399  
7400  /**
7401   * Returns a localized string.
7402   *
7403   * Returns the translated string specified by $identifier as
7404   * for $module.  Uses the same format files as STphp.
7405   * $a is an object, string or number that can be used
7406   * within translation strings
7407   *
7408   * eg 'hello {$a->firstname} {$a->lastname}'
7409   * or 'hello {$a}'
7410   *
7411   * If you would like to directly echo the localized string use
7412   * the function {@link print_string()}
7413   *
7414   * Example usage of this function involves finding the string you would
7415   * like a local equivalent of and using its identifier and module information
7416   * to retrieve it.<br/>
7417   * If you open moodle/lang/en/moodle.php and look near line 278
7418   * you will find a string to prompt a user for their word for 'course'
7419   * <code>
7420   * $string['course'] = 'Course';
7421   * </code>
7422   * So if you want to display the string 'Course'
7423   * in any language that supports it on your site
7424   * you just need to use the identifier 'course'
7425   * <code>
7426   * $mystring = '<strong>'. get_string('course') .'</strong>';
7427   * or
7428   * </code>
7429   * If the string you want is in another file you'd take a slightly
7430   * different approach. Looking in moodle/lang/en/calendar.php you find
7431   * around line 75:
7432   * <code>
7433   * $string['typecourse'] = 'Course event';
7434   * </code>
7435   * If you want to display the string "Course event" in any language
7436   * supported you would use the identifier 'typecourse' and the module 'calendar'
7437   * (because it is in the file calendar.php):
7438   * <code>
7439   * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7440   * </code>
7441   *
7442   * As a last resort, should the identifier fail to map to a string
7443   * the returned string will be [[ $identifier ]]
7444   *
7445   * In Moodle 2.3 there is a new argument to this function $lazyload.
7446   * Setting $lazyload to true causes get_string to return a lang_string object
7447   * rather than the string itself. The fetching of the string is then put off until
7448   * the string object is first used. The object can be used by calling it's out
7449   * method or by casting the object to a string, either directly e.g.
7450   *     (string)$stringobject
7451   * or indirectly by using the string within another string or echoing it out e.g.
7452   *     echo $stringobject
7453   *     return "<p>{$stringobject}</p>";
7454   * It is worth noting that using $lazyload and attempting to use the string as an
7455   * array key will cause a fatal error as objects cannot be used as array keys.
7456   * But you should never do that anyway!
7457   * For more information {@link lang_string}
7458   *
7459   * @category string
7460   * @param string $identifier The key identifier for the localized string
7461   * @param string $component The module where the key identifier is stored,
7462   *      usually expressed as the filename in the language pack without the
7463   *      .php on the end but can also be written as mod/forum or grade/export/xls.
7464   *      If none is specified then moodle.php is used.
7465   * @param string|object|array|int $a An object, string or number that can be used
7466   *      within translation strings
7467   * @param bool $lazyload If set to true a string object is returned instead of
7468   *      the string itself. The string then isn't calculated until it is first used.
7469   * @return string The localized string.
7470   * @throws coding_exception
7471   */
7472  function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7473      global $CFG;
7474  
7475      // If the lazy load argument has been supplied return a lang_string object
7476      // instead.
7477      // We need to make sure it is true (and a bool) as you will see below there
7478      // used to be a forth argument at one point.
7479      if ($lazyload === true) {
7480          return new lang_string($identifier, $component, $a);
7481      }
7482  
7483      if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7484          throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7485      }
7486  
7487      // There is now a forth argument again, this time it is a boolean however so
7488      // we can still check for the old extralocations parameter.
7489      if (!is_bool($lazyload) && !empty($lazyload)) {
7490          debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7491      }
7492  
7493      if (strpos((string)$component, '/') !== false) {
7494          debugging('The module name you passed to get_string is the deprecated format ' .
7495                  'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7496          $componentpath = explode('/', $component);
7497  
7498          switch ($componentpath[0]) {
7499              case 'mod':
7500                  $component = $componentpath[1];
7501                  break;
7502              case 'blocks':
7503              case 'block':
7504                  $component = 'block_'.$componentpath[1];
7505                  break;
7506              case 'enrol':
7507                  $component = 'enrol_'.$componentpath[1];
7508                  break;
7509              case 'format':
7510                  $component = 'format_'.$componentpath[1];
7511                  break;
7512              case 'grade':
7513                  $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7514                  break;
7515          }
7516      }
7517  
7518      $result = get_string_manager()->get_string($identifier, $component, $a);
7519  
7520      // Debugging feature lets you display string identifier and component.
7521      if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7522          $result .= ' {' . $identifier . '/' . $component . '}';
7523      }
7524      return $result;
7525  }
7526  
7527  /**
7528   * Converts an array of strings to their localized value.
7529   *
7530   * @param array $array An array of strings
7531   * @param string $component The language module that these strings can be found in.
7532   * @return stdClass translated strings.
7533   */
7534  function get_strings($array, $component = '') {
7535      $string = new stdClass;
7536      foreach ($array as $item) {
7537          $string->$item = get_string($item, $component);
7538      }
7539      return $string;
7540  }
7541  
7542  /**
7543   * Prints out a translated string.
7544   *
7545   * Prints out a translated string using the return value from the {@link get_string()} function.
7546   *
7547   * Example usage of this function when the string is in the moodle.php file:<br/>
7548   * <code>
7549   * echo '<strong>';
7550   * print_string('course');
7551   * echo '</strong>';
7552   * </code>
7553   *
7554   * Example usage of this function when the string is not in the moodle.php file:<br/>
7555   * <code>
7556   * echo '<h1>';
7557   * print_string('typecourse', 'calendar');
7558   * echo '</h1>';
7559   * </code>
7560   *
7561   * @category string
7562   * @param string $identifier The key identifier for the localized string
7563   * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7564   * @param string|object|array $a An object, string or number that can be used within translation strings
7565   */
7566  function print_string($identifier, $component = '', $a = null) {
7567      echo get_string($identifier, $component, $a);
7568  }
7569  
7570  /**
7571   * Returns a list of charset codes
7572   *
7573   * Returns a list of charset codes. It's hardcoded, so they should be added manually
7574   * (checking that such charset is supported by the texlib library!)
7575   *
7576   * @return array And associative array with contents in the form of charset => charset
7577   */
7578  function get_list_of_charsets() {
7579  
7580      $charsets = array(
7581          'EUC-JP'     => 'EUC-JP',
7582          'ISO-2022-JP'=> 'ISO-2022-JP',
7583          'ISO-8859-1' => 'ISO-8859-1',
7584          'SHIFT-JIS'  => 'SHIFT-JIS',
7585          'GB2312'     => 'GB2312',
7586          'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7587          'UTF-8'      => 'UTF-8');
7588  
7589      asort($charsets);
7590  
7591      return $charsets;
7592  }
7593  
7594  /**
7595   * Returns a list of valid and compatible themes
7596   *
7597   * @return array
7598   */
7599  function get_list_of_themes() {
7600      global $CFG;
7601  
7602      $themes = array();
7603  
7604      if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7605          $themelist = explode(',', $CFG->themelist);
7606      } else {
7607          $themelist = array_keys(core_component::get_plugin_list("theme"));
7608      }
7609  
7610      foreach ($themelist as $key => $themename) {
7611          $theme = theme_config::load($themename);
7612          $themes[$themename] = $theme;
7613      }
7614  
7615      core_collator::asort_objects_by_method($themes, 'get_theme_name');
7616  
7617      return $themes;
7618  }
7619  
7620  /**
7621   * Factory function for emoticon_manager
7622   *
7623   * @return emoticon_manager singleton
7624   */
7625  function get_emoticon_manager() {
7626      static $singleton = null;
7627  
7628      if (is_null($singleton)) {
7629          $singleton = new emoticon_manager();
7630      }
7631  
7632      return $singleton;
7633  }
7634  
7635  /**
7636   * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7637   *
7638   * Whenever this manager mentiones 'emoticon object', the following data
7639   * structure is expected: stdClass with properties text, imagename, imagecomponent,
7640   * altidentifier and altcomponent
7641   *
7642   * @see admin_setting_emoticons
7643   *
7644   * @copyright 2010 David Mudrak
7645   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7646   */
7647  class emoticon_manager {
7648  
7649      /**
7650       * Returns the currently enabled emoticons
7651       *
7652       * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7653       * @return array of emoticon objects
7654       */
7655      public function get_emoticons($selectable = false) {
7656          global $CFG;
7657          $notselectable = ['martin', 'egg'];
7658  
7659          if (empty($CFG->emoticons)) {
7660              return array();
7661          }
7662  
7663          $emoticons = $this->decode_stored_config($CFG->emoticons);
7664  
7665          if (!is_array($emoticons)) {
7666              // Something is wrong with the format of stored setting.
7667              debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7668              return array();
7669          }
7670          if ($selectable) {
7671              foreach ($emoticons as $index => $emote) {
7672                  if (in_array($emote->altidentifier, $notselectable)) {
7673                      // Skip this one.
7674                      unset($emoticons[$index]);
7675                  }
7676              }
7677          }
7678  
7679          return $emoticons;
7680      }
7681  
7682      /**
7683       * Converts emoticon object into renderable pix_emoticon object
7684       *
7685       * @param stdClass $emoticon emoticon object
7686       * @param array $attributes explicit HTML attributes to set
7687       * @return pix_emoticon
7688       */
7689      public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7690          $stringmanager = get_string_manager();
7691          if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7692              $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7693          } else {
7694              $alt = s($emoticon->text);
7695          }
7696          return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7697      }
7698  
7699      /**
7700       * Encodes the array of emoticon objects into a string storable in config table
7701       *
7702       * @see self::decode_stored_config()
7703       * @param array $emoticons array of emtocion objects
7704       * @return string
7705       */
7706      public function encode_stored_config(array $emoticons) {
7707          return json_encode($emoticons);
7708      }
7709  
7710      /**
7711       * Decodes the string into an array of emoticon objects
7712       *
7713       * @see self::encode_stored_config()
7714       * @param string $encoded
7715       * @return array|null
7716       */
7717      public function decode_stored_config($encoded) {
7718          $decoded = json_decode($encoded);
7719          if (!is_array($decoded)) {
7720              return null;
7721          }
7722          return $decoded;
7723      }
7724  
7725      /**
7726       * Returns default set of emoticons supported by Moodle
7727       *
7728       * @return array of sdtClasses
7729       */
7730      public function default_emoticons() {
7731          return array(
7732              $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7733              $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7734              $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7735              $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7736              $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7737              $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7738              $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7739              $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7740              $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7741              $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7742              $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7743              $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7744              $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7745              $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7746              $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7747              $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7748              $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7749              $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7750              $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7751              $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7752              $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7753              $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7754              $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7755              $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7756              $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7757              $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7758              $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7759              $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7760              $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7761              $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7762          );
7763      }
7764  
7765      /**
7766       * Helper method preparing the stdClass with the emoticon properties
7767       *
7768       * @param string|array $text or array of strings
7769       * @param string $imagename to be used by {@link pix_emoticon}
7770       * @param string $altidentifier alternative string identifier, null for no alt
7771       * @param string $altcomponent where the alternative string is defined
7772       * @param string $imagecomponent to be used by {@link pix_emoticon}
7773       * @return stdClass
7774       */
7775      protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7776                                                 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7777          return (object)array(
7778              'text'           => $text,
7779              'imagename'      => $imagename,
7780              'imagecomponent' => $imagecomponent,
7781              'altidentifier'  => $altidentifier,
7782              'altcomponent'   => $altcomponent,
7783          );
7784      }
7785  }
7786  
7787  // ENCRYPTION.
7788  
7789  /**
7790   * rc4encrypt
7791   *
7792   * @param string $data        Data to encrypt.
7793   * @return string             The now encrypted data.
7794   */
7795  function rc4encrypt($data) {
7796      return endecrypt(get_site_identifier(), $data, '');
7797  }
7798  
7799  /**
7800   * rc4decrypt
7801   *
7802   * @param string $data        Data to decrypt.
7803   * @return string             The now decrypted data.
7804   */
7805  function rc4decrypt($data) {
7806      return endecrypt(get_site_identifier(), $data, 'de');
7807  }
7808  
7809  /**
7810   * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7811   *
7812   * @todo Finish documenting this function
7813   *
7814   * @param string $pwd The password to use when encrypting or decrypting
7815   * @param string $data The data to be decrypted/encrypted
7816   * @param string $case Either 'de' for decrypt or '' for encrypt
7817   * @return string
7818   */
7819  function endecrypt ($pwd, $data, $case) {
7820  
7821      if ($case == 'de') {
7822          $data = urldecode($data);
7823      }
7824  
7825      $key[] = '';
7826      $box[] = '';
7827      $pwdlength = strlen($pwd);
7828  
7829      for ($i = 0; $i <= 255; $i++) {
7830          $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7831          $box[$i] = $i;
7832      }
7833  
7834      $x = 0;
7835  
7836      for ($i = 0; $i <= 255; $i++) {
7837          $x = ($x + $box[$i] + $key[$i]) % 256;
7838          $tempswap = $box[$i];
7839          $box[$i] = $box[$x];
7840          $box[$x] = $tempswap;
7841      }
7842  
7843      $cipher = '';
7844  
7845      $a = 0;
7846      $j = 0;
7847  
7848      for ($i = 0; $i < strlen($data); $i++) {
7849          $a = ($a + 1) % 256;
7850          $j = ($j + $box[$a]) % 256;
7851          $temp = $box[$a];
7852          $box[$a] = $box[$j];
7853          $box[$j] = $temp;
7854          $k = $box[(($box[$a] + $box[$j]) % 256)];
7855          $cipherby = ord(substr($data, $i, 1)) ^ $k;
7856          $cipher .= chr($cipherby);
7857      }
7858  
7859      if ($case == 'de') {
7860          $cipher = urldecode(urlencode($cipher));
7861      } else {
7862          $cipher = urlencode($cipher);
7863      }
7864  
7865      return $cipher;
7866  }
7867  
7868  // ENVIRONMENT CHECKING.
7869  
7870  /**
7871   * This method validates a plug name. It is much faster than calling clean_param.
7872   *
7873   * @param string $name a string that might be a plugin name.
7874   * @return bool if this string is a valid plugin name.
7875   */
7876  function is_valid_plugin_name($name) {
7877      // This does not work for 'mod', bad luck, use any other type.
7878      return core_component::is_valid_plugin_name('tool', $name);
7879  }
7880  
7881  /**
7882   * Get a list of all the plugins of a given type that define a certain API function
7883   * in a certain file. The plugin component names and function names are returned.
7884   *
7885   * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7886   * @param string $function the part of the name of the function after the
7887   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7888   *      names like report_courselist_hook.
7889   * @param string $file the name of file within the plugin that defines the
7890   *      function. Defaults to lib.php.
7891   * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7892   *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7893   */
7894  function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7895      global $CFG;
7896  
7897      // We don't include here as all plugin types files would be included.
7898      $plugins = get_plugins_with_function($function, $file, false);
7899  
7900      if (empty($plugins[$plugintype])) {
7901          return array();
7902      }
7903  
7904      $allplugins = core_component::get_plugin_list($plugintype);
7905  
7906      // Reformat the array and include the files.
7907      $pluginfunctions = array();
7908      foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7909  
7910          // Check that it has not been removed and the file is still available.
7911          if (!empty($allplugins[$pluginname])) {
7912  
7913              $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7914              if (file_exists($filepath)) {
7915                  include_once($filepath);
7916  
7917                  // Now that the file is loaded, we must verify the function still exists.
7918                  if (function_exists($functionname)) {
7919                      $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7920                  } else {
7921                      // Invalidate the cache for next run.
7922                      \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7923                  }
7924              }
7925          }
7926      }
7927  
7928      return $pluginfunctions;
7929  }
7930  
7931  /**
7932   * Get a list of all the plugins that define a certain API function in a certain file.
7933   *
7934   * @param string $function the part of the name of the function after the
7935   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7936   *      names like report_courselist_hook.
7937   * @param string $file the name of file within the plugin that defines the
7938   *      function. Defaults to lib.php.
7939   * @param bool $include Whether to include the files that contain the functions or not.
7940   * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7941   * @return array with [plugintype][plugin] = functionname
7942   */
7943  function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7944      global $CFG;
7945  
7946      if (during_initial_install() || isset($CFG->upgraderunning)) {
7947          // API functions _must not_ be called during an installation or upgrade.
7948          return [];
7949      }
7950  
7951      $plugincallback = $function;
7952      $filtermigrated = function($plugincallback, $pluginfunctions): array {
7953          foreach ($pluginfunctions as $plugintype => $plugins) {
7954              foreach ($plugins as $plugin => $unusedfunction) {
7955                  $component = $plugintype . '_' . $plugin;
7956                  if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($plugincallback)) {
7957                      if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) {
7958                          // Ignore the old callback, it is there only for older Moodle versions.
7959                          unset($pluginfunctions[$plugintype][$plugin]);
7960                      } else {
7961                          debugging("Callback $plugincallback in $component component should be migrated to new hook callback",
7962                              DEBUG_DEVELOPER);
7963                      }
7964                  }
7965              }
7966          }
7967          return $pluginfunctions;
7968      };
7969  
7970      $cache = \cache::make('core', 'plugin_functions');
7971  
7972      // Including both although I doubt that we will find two functions definitions with the same name.
7973      // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7974      $pluginfunctions = false;
7975      if (!empty($CFG->allversionshash)) {
7976          $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7977          $pluginfunctions = $cache->get($key);
7978      }
7979      $dirty = false;
7980  
7981      // Use the plugin manager to check that plugins are currently installed.
7982      $pluginmanager = \core_plugin_manager::instance();
7983  
7984      if ($pluginfunctions !== false) {
7985  
7986          // Checking that the files are still available.
7987          foreach ($pluginfunctions as $plugintype => $plugins) {
7988  
7989              $allplugins = \core_component::get_plugin_list($plugintype);
7990              $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7991              foreach ($plugins as $plugin => $function) {
7992                  if (!isset($installedplugins[$plugin])) {
7993                      // Plugin code is still present on disk but it is not installed.
7994                      $dirty = true;
7995                      break 2;
7996                  }
7997  
7998                  // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7999                  if (empty($allplugins[$plugin])) {
8000                      $dirty = true;
8001                      break 2;
8002                  }
8003  
8004                  $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
8005                  if ($include && $fileexists) {
8006                      // Include the files if it was requested.
8007                      include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
8008                  } else if (!$fileexists) {
8009                      // If the file is not available any more it should not be returned.
8010                      $dirty = true;
8011                      break 2;
8012                  }
8013  
8014                  // Check if the function still exists in the file.
8015                  if ($include && !function_exists($function)) {
8016                      $dirty = true;
8017                      break 2;
8018                  }
8019              }
8020          }
8021  
8022          // If the cache is dirty, we should fall through and let it rebuild.
8023          if (!$dirty) {
8024              if ($migratedtohook && $file === 'lib.php') {
8025                  $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
8026              }
8027              return $pluginfunctions;
8028          }
8029      }
8030  
8031      $pluginfunctions = array();
8032  
8033      // To fill the cached. Also, everything should continue working with cache disabled.
8034      $plugintypes = \core_component::get_plugin_types();
8035      foreach ($plugintypes as $plugintype => $unused) {
8036  
8037          // We need to include files here.
8038          $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
8039          $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
8040          foreach ($pluginswithfile as $plugin => $notused) {
8041  
8042              if (!isset($installedplugins[$plugin])) {
8043                  continue;
8044              }
8045  
8046              $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
8047  
8048              $pluginfunction = false;
8049              if (function_exists($fullfunction)) {
8050                  // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
8051                  $pluginfunction = $fullfunction;
8052  
8053              } else if ($plugintype === 'mod') {
8054                  // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
8055                  $shortfunction = $plugin . '_' . $function;
8056                  if (function_exists($shortfunction)) {
8057                      $pluginfunction = $shortfunction;
8058                  }
8059              }
8060  
8061              if ($pluginfunction) {
8062                  if (empty($pluginfunctions[$plugintype])) {
8063                      $pluginfunctions[$plugintype] = array();
8064                  }
8065                  $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
8066              }
8067  
8068          }
8069      }
8070      if (!empty($CFG->allversionshash)) {
8071          $cache->set($key, $pluginfunctions);
8072      }
8073  
8074      if ($migratedtohook && $file === 'lib.php') {
8075          $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
8076      }
8077  
8078      return $pluginfunctions;
8079  
8080  }
8081  
8082  /**
8083   * Lists plugin-like directories within specified directory
8084   *
8085   * This function was originally used for standard Moodle plugins, please use
8086   * new core_component::get_plugin_list() now.
8087   *
8088   * This function is used for general directory listing and backwards compatility.
8089   *
8090   * @param string $directory relative directory from root
8091   * @param string $exclude dir name to exclude from the list (defaults to none)
8092   * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
8093   * @return array Sorted array of directory names found under the requested parameters
8094   */
8095  function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
8096      global $CFG;
8097  
8098      $plugins = array();
8099  
8100      if (empty($basedir)) {
8101          $basedir = $CFG->dirroot .'/'. $directory;
8102  
8103      } else {
8104          $basedir = $basedir .'/'. $directory;
8105      }
8106  
8107      if ($CFG->debugdeveloper and empty($exclude)) {
8108          // Make sure devs do not use this to list normal plugins,
8109          // this is intended for general directories that are not plugins!
8110  
8111          $subtypes = core_component::get_plugin_types();
8112          if (in_array($basedir, $subtypes)) {
8113              debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
8114          }
8115          unset($subtypes);
8116      }
8117  
8118      $ignorelist = array_flip(array_filter([
8119          'CVS',
8120          '_vti_cnf',
8121          'amd',
8122          'classes',
8123          'simpletest',
8124          'tests',
8125          'templates',
8126          'yui',
8127          $exclude,
8128      ]));
8129  
8130      if (file_exists($basedir) && filetype($basedir) == 'dir') {
8131          if (!$dirhandle = opendir($basedir)) {
8132              debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8133              return array();
8134          }
8135          while (false !== ($dir = readdir($dirhandle))) {
8136              if (strpos($dir, '.') === 0) {
8137                  // Ignore directories starting with .
8138                  // These are treated as hidden directories.
8139                  continue;
8140              }
8141              if (array_key_exists($dir, $ignorelist)) {
8142                  // This directory features on the ignore list.
8143                  continue;
8144              }
8145              if (filetype($basedir .'/'. $dir) != 'dir') {
8146                  continue;
8147              }
8148              $plugins[] = $dir;
8149          }
8150          closedir($dirhandle);
8151      }
8152      if ($plugins) {
8153          asort($plugins);
8154      }
8155      return $plugins;
8156  }
8157  
8158  /**
8159   * Invoke plugin's callback functions
8160   *
8161   * @param string $type plugin type e.g. 'mod'
8162   * @param string $name plugin name
8163   * @param string $feature feature name
8164   * @param string $action feature's action
8165   * @param array $params parameters of callback function, should be an array
8166   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8167   * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
8168   * @return mixed
8169   *
8170   * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8171   */
8172  function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
8173      return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
8174  }
8175  
8176  /**
8177   * Invoke component's callback functions
8178   *
8179   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8180   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8181   * @param array $params parameters of callback function
8182   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8183   * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
8184   * @return mixed
8185   */
8186  function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
8187  
8188      $functionname = component_callback_exists($component, $function);
8189  
8190      if ($params && (array_keys($params) !== range(0, count($params) - 1))) {
8191          // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but
8192          // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions.
8193          // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array
8194          // This check can be removed when minimum PHP version for Moodle is raised to 8.
8195          debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.',
8196              DEBUG_DEVELOPER);
8197          $params = array_values($params);
8198      }
8199  
8200      if ($functionname) {
8201          if ($migratedtohook) {
8202              if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($function)) {
8203                  if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) {
8204                      // Do not call the old lib.php callback,
8205                      // it is there for compatibility with older Moodle versions only.
8206                      return null;
8207                  } else {
8208                      debugging("Callback $function in $component component should be migrated to new hook callback",
8209                          DEBUG_DEVELOPER);
8210                  }
8211              }
8212          }
8213  
8214          // Function exists, so just return function result.
8215          $ret = call_user_func_array($functionname, $params);
8216          if (is_null($ret)) {
8217              return $default;
8218          } else {
8219              return $ret;
8220          }
8221      }
8222      return $default;
8223  }
8224  
8225  /**
8226   * Determine if a component callback exists and return the function name to call. Note that this
8227   * function will include the required library files so that the functioname returned can be
8228   * called directly.
8229   *
8230   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8231   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8232   * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8233   * @throws coding_exception if invalid component specfied
8234   */
8235  function component_callback_exists($component, $function) {
8236      global $CFG; // This is needed for the inclusions.
8237  
8238      $cleancomponent = clean_param($component, PARAM_COMPONENT);
8239      if (empty($cleancomponent)) {
8240          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8241      }
8242      $component = $cleancomponent;
8243  
8244      list($type, $name) = core_component::normalize_component($component);
8245      $component = $type . '_' . $name;
8246  
8247      $oldfunction = $name.'_'.$function;
8248      $function = $component.'_'.$function;
8249  
8250      $dir = core_component::get_component_directory($component);
8251      if (empty($dir)) {
8252          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8253      }
8254  
8255      // Load library and look for function.
8256      if (file_exists($dir.'/lib.php')) {
8257          require_once($dir.'/lib.php');
8258      }
8259  
8260      if (!function_exists($function) and function_exists($oldfunction)) {
8261          if ($type !== 'mod' and $type !== 'core') {
8262              debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8263          }
8264          $function = $oldfunction;
8265      }
8266  
8267      if (function_exists($function)) {
8268          return $function;
8269      }
8270      return false;
8271  }
8272  
8273  /**
8274   * Call the specified callback method on the provided class.
8275   *
8276   * If the callback returns null, then the default value is returned instead.
8277   * If the class does not exist, then the default value is returned.
8278   *
8279   * @param   string      $classname The name of the class to call upon.
8280   * @param   string      $methodname The name of the staticically defined method on the class.
8281   * @param   array       $params The arguments to pass into the method.
8282   * @param   mixed       $default The default value.
8283   * @return  mixed       The return value.
8284   */
8285  function component_class_callback($classname, $methodname, array $params, $default = null) {
8286      if (!class_exists($classname)) {
8287          return $default;
8288      }
8289  
8290      if (!method_exists($classname, $methodname)) {
8291          return $default;
8292      }
8293  
8294      $fullfunction = $classname . '::' . $methodname;
8295      $result = call_user_func_array($fullfunction, $params);
8296  
8297      if (null === $result) {
8298          return $default;
8299      } else {
8300          return $result;
8301      }
8302  }
8303  
8304  /**
8305   * Checks whether a plugin supports a specified feature.
8306   *
8307   * @param string $type Plugin type e.g. 'mod'
8308   * @param string $name Plugin name e.g. 'forum'
8309   * @param string $feature Feature code (FEATURE_xx constant)
8310   * @param mixed $default default value if feature support unknown
8311   * @return mixed Feature result (false if not supported, null if feature is unknown,
8312   *         otherwise usually true but may have other feature-specific value such as array)
8313   * @throws coding_exception
8314   */
8315  function plugin_supports($type, $name, $feature, $default = null) {
8316      global $CFG;
8317  
8318      if ($type === 'mod' and $name === 'NEWMODULE') {
8319          // Somebody forgot to rename the module template.
8320          return false;
8321      }
8322  
8323      $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8324      if (empty($component)) {
8325          throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8326      }
8327  
8328      $function = null;
8329  
8330      if ($type === 'mod') {
8331          // We need this special case because we support subplugins in modules,
8332          // otherwise it would end up in infinite loop.
8333          if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8334              include_once("$CFG->dirroot/mod/$name/lib.php");
8335              $function = $component.'_supports';
8336              if (!function_exists($function)) {
8337                  // Legacy non-frankenstyle function name.
8338                  $function = $name.'_supports';
8339              }
8340          }
8341  
8342      } else {
8343          if (!$path = core_component::get_plugin_directory($type, $name)) {
8344              // Non existent plugin type.
8345              return false;
8346          }
8347          if (file_exists("$path/lib.php")) {
8348              include_once("$path/lib.php");
8349              $function = $component.'_supports';
8350          }
8351      }
8352  
8353      if ($function and function_exists($function)) {
8354          $supports = $function($feature);
8355          if (is_null($supports)) {
8356              // Plugin does not know - use default.
8357              return $default;
8358          } else {
8359              return $supports;
8360          }
8361      }
8362  
8363      // Plugin does not care, so use default.
8364      return $default;
8365  }
8366  
8367  /**
8368   * Returns true if the current version of PHP is greater that the specified one.
8369   *
8370   * @todo Check PHP version being required here is it too low?
8371   *
8372   * @param string $version The version of php being tested.
8373   * @return bool
8374   */
8375  function check_php_version($version='5.2.4') {
8376      return (version_compare(phpversion(), $version) >= 0);
8377  }
8378  
8379  /**
8380   * Determine if moodle installation requires update.
8381   *
8382   * Checks version numbers of main code and all plugins to see
8383   * if there are any mismatches.
8384   *
8385   * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8386   * @return bool
8387   */
8388  function moodle_needs_upgrading($checkupgradeflag = true) {
8389      global $CFG, $DB;
8390  
8391      // Say no if there is already an upgrade running.
8392      if ($checkupgradeflag) {
8393          $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8394          $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
8395          // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8396          // We should always return false.
8397          // This means the upgrade is running from CLI somewhere, or about to.
8398          if (!empty($lock) && !$currentprocessrunningupgrade) {
8399              return false;
8400          }
8401      }
8402  
8403      if (empty($CFG->version)) {
8404          return true;
8405      }
8406  
8407      // There is no need to purge plugininfo caches here because
8408      // these caches are not used during upgrade and they are purged after
8409      // every upgrade.
8410  
8411      if (empty($CFG->allversionshash)) {
8412          return true;
8413      }
8414  
8415      $hash = core_component::get_all_versions_hash();
8416  
8417      return ($hash !== $CFG->allversionshash);
8418  }
8419  
8420  /**
8421   * Returns the major version of this site
8422   *
8423   * Moodle version numbers consist of three numbers separated by a dot, for
8424   * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8425   * called major version. This function extracts the major version from either
8426   * $CFG->release (default) or eventually from the $release variable defined in
8427   * the main version.php.
8428   *
8429   * @param bool $fromdisk should the version if source code files be used
8430   * @return string|false the major version like '2.3', false if could not be determined
8431   */
8432  function moodle_major_version($fromdisk = false) {
8433      global $CFG;
8434  
8435      if ($fromdisk) {
8436          $release = null;
8437          require($CFG->dirroot.'/version.php');
8438          if (empty($release)) {
8439              return false;
8440          }
8441  
8442      } else {
8443          if (empty($CFG->release)) {
8444              return false;
8445          }
8446          $release = $CFG->release;
8447      }
8448  
8449      if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8450          return $matches[0];
8451      } else {
8452          return false;
8453      }
8454  }
8455  
8456  // MISCELLANEOUS.
8457  
8458  /**
8459   * Gets the system locale
8460   *
8461   * @return string Retuns the current locale.
8462   */
8463  function moodle_getlocale() {
8464      global $CFG;
8465  
8466      // Fetch the correct locale based on ostype.
8467      if ($CFG->ostype == 'WINDOWS') {
8468          $stringtofetch = 'localewin';
8469      } else {
8470          $stringtofetch = 'locale';
8471      }
8472  
8473      if (!empty($CFG->locale)) { // Override locale for all language packs.
8474          return $CFG->locale;
8475      }
8476  
8477      return get_string($stringtofetch, 'langconfig');
8478  }
8479  
8480  /**
8481   * Sets the system locale
8482   *
8483   * @category string
8484   * @param string $locale Can be used to force a locale
8485   */
8486  function moodle_setlocale($locale='') {
8487      global $CFG;
8488  
8489      static $currentlocale = ''; // Last locale caching.
8490  
8491      $oldlocale = $currentlocale;
8492  
8493      // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8494      if (!empty($locale)) {
8495          $currentlocale = $locale;
8496      } else {
8497          $currentlocale = moodle_getlocale();
8498      }
8499  
8500      // Do nothing if locale already set up.
8501      if ($oldlocale == $currentlocale) {
8502          return;
8503      }
8504  
8505      // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8506      // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8507      // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8508  
8509      // Get current values.
8510      $monetary= setlocale (LC_MONETARY, 0);
8511      $numeric = setlocale (LC_NUMERIC, 0);
8512      $ctype   = setlocale (LC_CTYPE, 0);
8513      if ($CFG->ostype != 'WINDOWS') {
8514          $messages= setlocale (LC_MESSAGES, 0);
8515      }
8516      // Set locale to all.
8517      $result = setlocale (LC_ALL, $currentlocale);
8518      // If setting of locale fails try the other utf8 or utf-8 variant,
8519      // some operating systems support both (Debian), others just one (OSX).
8520      if ($result === false) {
8521          if (stripos($currentlocale, '.UTF-8') !== false) {
8522              $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8523              setlocale (LC_ALL, $newlocale);
8524          } else if (stripos($currentlocale, '.UTF8') !== false) {
8525              $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8526              setlocale (LC_ALL, $newlocale);
8527          }
8528      }
8529      // Set old values.
8530      setlocale (LC_MONETARY, $monetary);
8531      setlocale (LC_NUMERIC, $numeric);
8532      if ($CFG->ostype != 'WINDOWS') {
8533          setlocale (LC_MESSAGES, $messages);
8534      }
8535      if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8536          // To workaround a well-known PHP problem with Turkish letter Ii.
8537          setlocale (LC_CTYPE, $ctype);
8538      }
8539  }
8540  
8541  /**
8542   * Count words in a string.
8543   *
8544   * Words are defined as things between whitespace.
8545   *
8546   * @category string
8547   * @param string $string The text to be searched for words. May be HTML.
8548   * @param int|null $format
8549   * @return int The count of words in the specified string
8550   */
8551  function count_words($string, $format = null) {
8552      // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8553      // Also, br is a special case because it definitely delimits a word, but has no close tag.
8554      $string = preg_replace('~
8555              (                                   # Capture the tag we match.
8556                  </                              # Start of close tag.
8557                  (?!                             # Do not match any of these specific close tag names.
8558                      a> | b> | del> | em> | i> |
8559                      ins> | s> | small> | span> |
8560                      strong> | sub> | sup> | u>
8561                  )
8562                  \w+                             # But, apart from those execptions, match any tag name.
8563                  >                               # End of close tag.
8564              |
8565                  <br> | <br\s*/>                 # Special cases that are not close tags.
8566              )
8567              ~x', '$1 ', $string); // Add a space after the close tag.
8568      if ($format !== null && $format != FORMAT_PLAIN) {
8569          // Match the usual text cleaning before display.
8570          // Ideally we should apply multilang filter only here, other filters might add extra text.
8571          $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8572      }
8573      // Now remove HTML tags.
8574      $string = strip_tags($string);
8575      // Decode HTML entities.
8576      $string = html_entity_decode($string, ENT_COMPAT);
8577  
8578      // Now, the word count is the number of blocks of characters separated
8579      // by any sort of space. That seems to be the definition used by all other systems.
8580      // To be precise about what is considered to separate words:
8581      // * Anything that Unicode considers a 'Separator'
8582      // * Anything that Unicode considers a 'Control character'
8583      // * An em- or en- dash.
8584      return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8585  }
8586  
8587  /**
8588   * Count letters in a string.
8589   *
8590   * Letters are defined as chars not in tags and different from whitespace.
8591   *
8592   * @category string
8593   * @param string $string The text to be searched for letters. May be HTML.
8594   * @param int|null $format
8595   * @return int The count of letters in the specified text.
8596   */
8597  function count_letters($string, $format = null) {
8598      if ($format !== null && $format != FORMAT_PLAIN) {
8599          // Match the usual text cleaning before display.
8600          // Ideally we should apply multilang filter only here, other filters might add extra text.
8601          $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8602      }
8603      $string = strip_tags($string); // Tags are out now.
8604      $string = html_entity_decode($string, ENT_COMPAT);
8605      $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8606  
8607      return core_text::strlen($string);
8608  }
8609  
8610  /**
8611   * Generate and return a random string of the specified length.
8612   *
8613   * @param int $length The length of the string to be created.
8614   * @return string
8615   */
8616  function random_string($length=15) {
8617      $randombytes = random_bytes($length);
8618      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8619      $pool .= 'abcdefghijklmnopqrstuvwxyz';
8620      $pool .= '0123456789';
8621      $poollen = strlen($pool);
8622      $string = '';
8623      for ($i = 0; $i < $length; $i++) {
8624          $rand = ord($randombytes[$i]);
8625          $string .= substr($pool, ($rand%($poollen)), 1);
8626      }
8627      return $string;
8628  }
8629  
8630  /**
8631   * Generate a complex random string (useful for md5 salts)
8632   *
8633   * This function is based on the above {@link random_string()} however it uses a
8634   * larger pool of characters and generates a string between 24 and 32 characters
8635   *
8636   * @param int $length Optional if set generates a string to exactly this length
8637   * @return string
8638   */
8639  function complex_random_string($length=null) {
8640      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8641      $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8642      $poollen = strlen($pool);
8643      if ($length===null) {
8644          $length = floor(rand(24, 32));
8645      }
8646      $randombytes = random_bytes($length);
8647      $string = '';
8648      for ($i = 0; $i < $length; $i++) {
8649          $rand = ord($randombytes[$i]);
8650          $string .= $pool[($rand%$poollen)];
8651      }
8652      return $string;
8653  }
8654  
8655  /**
8656   * Given some text (which may contain HTML) and an ideal length,
8657   * this function truncates the text neatly on a word boundary if possible
8658   *
8659   * @category string
8660   * @param string $text text to be shortened
8661   * @param int $ideal ideal string length
8662   * @param boolean $exact if false, $text will not be cut mid-word
8663   * @param string $ending The string to append if the passed string is truncated
8664   * @return string $truncate shortened string
8665   */
8666  function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8667      // If the plain text is shorter than the maximum length, return the whole text.
8668      if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8669          return $text;
8670      }
8671  
8672      // Splits on HTML tags. Each open/close/empty tag will be the first thing
8673      // and only tag in its 'line'.
8674      preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8675  
8676      $totallength = core_text::strlen($ending);
8677      $truncate = '';
8678  
8679      // This array stores information about open and close tags and their position
8680      // in the truncated string. Each item in the array is an object with fields
8681      // ->open (true if open), ->tag (tag name in lower case), and ->pos
8682      // (byte position in truncated text).
8683      $tagdetails = array();
8684  
8685      foreach ($lines as $linematchings) {
8686          // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8687          if (!empty($linematchings[1])) {
8688              // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8689              if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8690                  if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8691                      // Record closing tag.
8692                      $tagdetails[] = (object) array(
8693                              'open' => false,
8694                              'tag'  => core_text::strtolower($tagmatchings[1]),
8695                              'pos'  => core_text::strlen($truncate),
8696                          );
8697  
8698                  } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8699                      // Record opening tag.
8700                      $tagdetails[] = (object) array(
8701                              'open' => true,
8702                              'tag'  => core_text::strtolower($tagmatchings[1]),
8703                              'pos'  => core_text::strlen($truncate),
8704                          );
8705                  } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8706                      $tagdetails[] = (object) array(
8707                              'open' => true,
8708                              'tag'  => core_text::strtolower('if'),
8709                              'pos'  => core_text::strlen($truncate),
8710                      );
8711                  } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8712                      $tagdetails[] = (object) array(
8713                              'open' => false,
8714                              'tag'  => core_text::strtolower('if'),
8715                              'pos'  => core_text::strlen($truncate),
8716                      );
8717                  }
8718              }
8719              // Add html-tag to $truncate'd text.
8720              $truncate .= $linematchings[1];
8721          }
8722  
8723          // Calculate the length of the plain text part of the line; handle entities as one character.
8724          $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8725          if ($totallength + $contentlength > $ideal) {
8726              // The number of characters which are left.
8727              $left = $ideal - $totallength;
8728              $entitieslength = 0;
8729              // Search for html entities.
8730              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)) {
8731                  // Calculate the real length of all entities in the legal range.
8732                  foreach ($entities[0] as $entity) {
8733                      if ($entity[1]+1-$entitieslength <= $left) {
8734                          $left--;
8735                          $entitieslength += core_text::strlen($entity[0]);
8736                      } else {
8737                          // No more characters left.
8738                          break;
8739                      }
8740                  }
8741              }
8742              $breakpos = $left + $entitieslength;
8743  
8744              // If the words shouldn't be cut in the middle...
8745              if (!$exact) {
8746                  // Search the last occurence of a space.
8747                  for (; $breakpos > 0; $breakpos--) {
8748                      if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8749                          if ($char === '.' or $char === ' ') {
8750                              $breakpos += 1;
8751                              break;
8752                          } else if (strlen($char) > 2) {
8753                              // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8754                              $breakpos += 1;
8755                              break;
8756                          }
8757                      }
8758                  }
8759              }
8760              if ($breakpos == 0) {
8761                  // This deals with the test_shorten_text_no_spaces case.
8762                  $breakpos = $left + $entitieslength;
8763              } else if ($breakpos > $left + $entitieslength) {
8764                  // This deals with the previous for loop breaking on the first char.
8765                  $breakpos = $left + $entitieslength;
8766              }
8767  
8768              $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8769              // Maximum length is reached, so get off the loop.
8770              break;
8771          } else {
8772              $truncate .= $linematchings[2];
8773              $totallength += $contentlength;
8774          }
8775  
8776          // If the maximum length is reached, get off the loop.
8777          if ($totallength >= $ideal) {
8778              break;
8779          }
8780      }
8781  
8782      // Add the defined ending to the text.
8783      $truncate .= $ending;
8784  
8785      // Now calculate the list of open html tags based on the truncate position.
8786      $opentags = array();
8787      foreach ($tagdetails as $taginfo) {
8788          if ($taginfo->open) {
8789              // Add tag to the beginning of $opentags list.
8790              array_unshift($opentags, $taginfo->tag);
8791          } else {
8792              // Can have multiple exact same open tags, close the last one.
8793              $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8794              if ($pos !== false) {
8795                  unset($opentags[$pos]);
8796              }
8797          }
8798      }
8799  
8800      // Close all unclosed html-tags.
8801      foreach ($opentags as $tag) {
8802          if ($tag === 'if') {
8803              $truncate .= '<!--<![endif]-->';
8804          } else {
8805              $truncate .= '</' . $tag . '>';
8806          }
8807      }
8808  
8809      return $truncate;
8810  }
8811  
8812  /**
8813   * Shortens a given filename by removing characters positioned after the ideal string length.
8814   * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8815   * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8816   *
8817   * @param string $filename file name
8818   * @param int $length ideal string length
8819   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8820   * @return string $shortened shortened file name
8821   */
8822  function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8823      $shortened = $filename;
8824      // Extract a part of the filename if it's char size exceeds the ideal string length.
8825      if (core_text::strlen($filename) > $length) {
8826          // Exclude extension if present in filename.
8827          $mimetypes = get_mimetypes_array();
8828          $extension = pathinfo($filename, PATHINFO_EXTENSION);
8829          if ($extension && !empty($mimetypes[$extension])) {
8830              $basename = pathinfo($filename, PATHINFO_FILENAME);
8831              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8832              $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8833              $shortened .= '.' . $extension;
8834          } else {
8835              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8836              $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8837          }
8838      }
8839      return $shortened;
8840  }
8841  
8842  /**
8843   * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8844   *
8845   * @param array $path The paths to reduce the length.
8846   * @param int $length Ideal string length
8847   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8848   * @return array $result Shortened paths in array.
8849   */
8850  function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8851      $result = null;
8852  
8853      $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8854          $carry[] = shorten_filename($singlepath, $length, $includehash);
8855          return $carry;
8856      }, []);
8857  
8858      return $result;
8859  }
8860  
8861  /**
8862   * Given dates in seconds, how many weeks is the date from startdate
8863   * The first week is 1, the second 2 etc ...
8864   *
8865   * @param int $startdate Timestamp for the start date
8866   * @param int $thedate Timestamp for the end date
8867   * @return string
8868   */
8869  function getweek ($startdate, $thedate) {
8870      if ($thedate < $startdate) {
8871          return 0;
8872      }
8873  
8874      return floor(($thedate - $startdate) / WEEKSECS) + 1;
8875  }
8876  
8877  /**
8878   * Returns a randomly generated password of length $maxlen.  inspired by
8879   *
8880   * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8881   * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8882   *
8883   * @param int $maxlen  The maximum size of the password being generated.
8884   * @return string
8885   */
8886  function generate_password($maxlen=10) {
8887      global $CFG;
8888  
8889      if (empty($CFG->passwordpolicy)) {
8890          $fillers = PASSWORD_DIGITS;
8891          $wordlist = file($CFG->wordlist);
8892          $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8893          $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8894          $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8895          $password = $word1 . $filler1 . $word2;
8896      } else {
8897          $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8898          $digits = $CFG->minpassworddigits;
8899          $lower = $CFG->minpasswordlower;
8900          $upper = $CFG->minpasswordupper;
8901          $nonalphanum = $CFG->minpasswordnonalphanum;
8902          $total = $lower + $upper + $digits + $nonalphanum;
8903          // Var minlength should be the greater one of the two ( $minlen and $total ).
8904          $minlen = $minlen < $total ? $total : $minlen;
8905          // Var maxlen can never be smaller than minlen.
8906          $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8907          $additional = $maxlen - $total;
8908  
8909          // Make sure we have enough characters to fulfill
8910          // complexity requirements.
8911          $passworddigits = PASSWORD_DIGITS;
8912          while ($digits > strlen($passworddigits)) {
8913              $passworddigits .= PASSWORD_DIGITS;
8914          }
8915          $passwordlower = PASSWORD_LOWER;
8916          while ($lower > strlen($passwordlower)) {
8917              $passwordlower .= PASSWORD_LOWER;
8918          }
8919          $passwordupper = PASSWORD_UPPER;
8920          while ($upper > strlen($passwordupper)) {
8921              $passwordupper .= PASSWORD_UPPER;
8922          }
8923          $passwordnonalphanum = PASSWORD_NONALPHANUM;
8924          while ($nonalphanum > strlen($passwordnonalphanum)) {
8925              $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8926          }
8927  
8928          // Now mix and shuffle it all.
8929          $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8930                                   substr(str_shuffle ($passwordupper), 0, $upper) .
8931                                   substr(str_shuffle ($passworddigits), 0, $digits) .
8932                                   substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8933                                   substr(str_shuffle ($passwordlower .
8934                                                       $passwordupper .
8935                                                       $passworddigits .
8936                                                       $passwordnonalphanum), 0 , $additional));
8937      }
8938  
8939      return substr ($password, 0, $maxlen);
8940  }
8941  
8942  /**
8943   * Given a float, prints it nicely.
8944   * Localized floats must not be used in calculations!
8945   *
8946   * The stripzeros feature is intended for making numbers look nicer in small
8947   * areas where it is not necessary to indicate the degree of accuracy by showing
8948   * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8949   * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8950   *
8951   * @param float $float The float to print
8952   * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8953   * @param bool $localized use localized decimal separator
8954   * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8955   *                         the decimal point are always striped if $decimalpoints is -1.
8956   * @return string locale float
8957   */
8958  function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8959      if (is_null($float)) {
8960          return '';
8961      }
8962      if ($localized) {
8963          $separator = get_string('decsep', 'langconfig');
8964      } else {
8965          $separator = '.';
8966      }
8967      if ($decimalpoints == -1) {
8968          // The following counts the number of decimals.
8969          // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8970          $floatval = floatval($float);
8971          for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8972      }
8973  
8974      $result = number_format($float, $decimalpoints, $separator, '');
8975      if ($stripzeros && $decimalpoints > 0) {
8976          // Remove zeros and final dot if not needed.
8977          // However, only do this if there is a decimal point!
8978          $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8979      }
8980      return $result;
8981  }
8982  
8983  /**
8984   * Converts locale specific floating point/comma number back to standard PHP float value
8985   * Do NOT try to do any math operations before this conversion on any user submitted floats!
8986   *
8987   * @param string $localefloat locale aware float representation
8988   * @param bool $strict If true, then check the input and return false if it is not a valid number.
8989   * @return mixed float|bool - false or the parsed float.
8990   */
8991  function unformat_float($localefloat, $strict = false) {
8992      $localefloat = trim((string)$localefloat);
8993  
8994      if ($localefloat == '') {
8995          return null;
8996      }
8997  
8998      $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8999      $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
9000  
9001      if ($strict && !is_numeric($localefloat)) {
9002          return false;
9003      }
9004  
9005      return (float)$localefloat;
9006  }
9007  
9008  /**
9009   * Given a simple array, this shuffles it up just like shuffle()
9010   * Unlike PHP's shuffle() this function works on any machine.
9011   *
9012   * @param array $array The array to be rearranged
9013   * @return array
9014   */
9015  function swapshuffle($array) {
9016  
9017      $last = count($array) - 1;
9018      for ($i = 0; $i <= $last; $i++) {
9019          $from = rand(0, $last);
9020          $curr = $array[$i];
9021          $array[$i] = $array[$from];
9022          $array[$from] = $curr;
9023      }
9024      return $array;
9025  }
9026  
9027  /**
9028   * Like {@link swapshuffle()}, but works on associative arrays
9029   *
9030   * @param array $array The associative array to be rearranged
9031   * @return array
9032   */
9033  function swapshuffle_assoc($array) {
9034  
9035      $newarray = array();
9036      $newkeys = swapshuffle(array_keys($array));
9037  
9038      foreach ($newkeys as $newkey) {
9039          $newarray[$newkey] = $array[$newkey];
9040      }
9041      return $newarray;
9042  }
9043  
9044  /**
9045   * Given an arbitrary array, and a number of draws,
9046   * this function returns an array with that amount
9047   * of items.  The indexes are retained.
9048   *
9049   * @todo Finish documenting this function
9050   *
9051   * @param array $array
9052   * @param int $draws
9053   * @return array
9054   */
9055  function draw_rand_array($array, $draws) {
9056  
9057      $return = array();
9058  
9059      $last = count($array);
9060  
9061      if ($draws > $last) {
9062          $draws = $last;
9063      }
9064  
9065      while ($draws > 0) {
9066          $last--;
9067  
9068          $keys = array_keys($array);
9069          $rand = rand(0, $last);
9070  
9071          $return[$keys[$rand]] = $array[$keys[$rand]];
9072          unset($array[$keys[$rand]]);
9073  
9074          $draws--;
9075      }
9076  
9077      return $return;
9078  }
9079  
9080  /**
9081   * Calculate the difference between two microtimes
9082   *
9083   * @param string $a The first Microtime
9084   * @param string $b The second Microtime
9085   * @return string
9086   */
9087  function microtime_diff($a, $b) {
9088      list($adec, $asec) = explode(' ', $a);
9089      list($bdec, $bsec) = explode(' ', $b);
9090      return $bsec - $asec + $bdec - $adec;
9091  }
9092  
9093  /**
9094   * Given a list (eg a,b,c,d,e) this function returns
9095   * an array of 1->a, 2->b, 3->c etc
9096   *
9097   * @param string $list The string to explode into array bits
9098   * @param string $separator The separator used within the list string
9099   * @return array The now assembled array
9100   */
9101  function make_menu_from_list($list, $separator=',') {
9102  
9103      $array = array_reverse(explode($separator, $list), true);
9104      foreach ($array as $key => $item) {
9105          $outarray[$key+1] = trim($item);
9106      }
9107      return $outarray;
9108  }
9109  
9110  /**
9111   * Creates an array that represents all the current grades that
9112   * can be chosen using the given grading type.
9113   *
9114   * Negative numbers
9115   * are scales, zero is no grade, and positive numbers are maximum
9116   * grades.
9117   *
9118   * @todo Finish documenting this function or better deprecated this completely!
9119   *
9120   * @param int $gradingtype
9121   * @return array
9122   */
9123  function make_grades_menu($gradingtype) {
9124      global $DB;
9125  
9126      $grades = array();
9127      if ($gradingtype < 0) {
9128          if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9129              return make_menu_from_list($scale->scale);
9130          }
9131      } else if ($gradingtype > 0) {
9132          for ($i=$gradingtype; $i>=0; $i--) {
9133              $grades[$i] = $i .' / '. $gradingtype;
9134          }
9135          return $grades;
9136      }
9137      return $grades;
9138  }
9139  
9140  /**
9141   * make_unique_id_code
9142   *
9143   * @todo Finish documenting this function
9144   *
9145   * @uses $_SERVER
9146   * @param string $extra Extra string to append to the end of the code
9147   * @return string
9148   */
9149  function make_unique_id_code($extra = '') {
9150  
9151      $hostname = 'unknownhost';
9152      if (!empty($_SERVER['HTTP_HOST'])) {
9153          $hostname = $_SERVER['HTTP_HOST'];
9154      } else if (!empty($_ENV['HTTP_HOST'])) {
9155          $hostname = $_ENV['HTTP_HOST'];
9156      } else if (!empty($_SERVER['SERVER_NAME'])) {
9157          $hostname = $_SERVER['SERVER_NAME'];
9158      } else if (!empty($_ENV['SERVER_NAME'])) {
9159          $hostname = $_ENV['SERVER_NAME'];
9160      }
9161  
9162      $date = gmdate("ymdHis");
9163  
9164      $random =  random_string(6);
9165  
9166      if ($extra) {
9167          return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9168      } else {
9169          return $hostname .'+'. $date .'+'. $random;
9170      }
9171  }
9172  
9173  
9174  /**
9175   * Function to check the passed address is within the passed subnet
9176   *
9177   * The parameter is a comma separated string of subnet definitions.
9178   * Subnet strings can be in one of three formats:
9179   *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
9180   *   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)
9181   *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
9182   * Code for type 1 modified from user posted comments by mediator at
9183   * {@link http://au.php.net/manual/en/function.ip2long.php}
9184   *
9185   * @param string $addr    The address you are checking
9186   * @param string $subnetstr    The string of subnet addresses
9187   * @param bool $checkallzeros    The state to whether check for 0.0.0.0
9188   * @return bool
9189   */
9190  function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
9191  
9192      if ($addr == '0.0.0.0' && !$checkallzeros) {
9193          return false;
9194      }
9195      $subnets = explode(',', $subnetstr);
9196      $found = false;
9197      $addr = trim($addr);
9198      $addr = cleanremoteaddr($addr, false); // Normalise.
9199      if ($addr === null) {
9200          return false;
9201      }
9202      $addrparts = explode(':', $addr);
9203  
9204      $ipv6 = strpos($addr, ':');
9205  
9206      foreach ($subnets as $subnet) {
9207          $subnet = trim($subnet);
9208          if ($subnet === '') {
9209              continue;
9210          }
9211  
9212          if (strpos($subnet, '/') !== false) {
9213              // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9214              list($ip, $mask) = explode('/', $subnet);
9215              $mask = trim($mask);
9216              if (!is_number($mask)) {
9217                  continue; // Incorect mask number, eh?
9218              }
9219              $ip = cleanremoteaddr($ip, false); // Normalise.
9220              if ($ip === null) {
9221                  continue;
9222              }
9223              if (strpos($ip, ':') !== false) {
9224                  // IPv6.
9225                  if (!$ipv6) {
9226                      continue;
9227                  }
9228                  if ($mask > 128 or $mask < 0) {
9229                      continue; // Nonsense.
9230                  }
9231                  if ($mask == 0) {
9232                      return true; // Any address.
9233                  }
9234                  if ($mask == 128) {
9235                      if ($ip === $addr) {
9236                          return true;
9237                      }
9238                      continue;
9239                  }
9240                  $ipparts = explode(':', $ip);
9241                  $modulo  = $mask % 16;
9242                  $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
9243                  $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9244                  if (implode(':', $ipnet) === implode(':', $addrnet)) {
9245                      if ($modulo == 0) {
9246                          return true;
9247                      }
9248                      $pos     = ($mask-$modulo)/16;
9249                      $ipnet   = hexdec($ipparts[$pos]);
9250                      $addrnet = hexdec($addrparts[$pos]);
9251                      $mask    = 0xffff << (16 - $modulo);
9252                      if (($addrnet & $mask) == ($ipnet & $mask)) {
9253                          return true;
9254                      }
9255                  }
9256  
9257              } else {
9258                  // IPv4.
9259                  if ($ipv6) {
9260                      continue;
9261                  }
9262                  if ($mask > 32 or $mask < 0) {
9263                      continue; // Nonsense.
9264                  }
9265                  if ($mask == 0) {
9266                      return true;
9267                  }
9268                  if ($mask == 32) {
9269                      if ($ip === $addr) {
9270                          return true;
9271                      }
9272                      continue;
9273                  }
9274                  $mask = 0xffffffff << (32 - $mask);
9275                  if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9276                      return true;
9277                  }
9278              }
9279  
9280          } else if (strpos($subnet, '-') !== false) {
9281              // 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.
9282              $parts = explode('-', $subnet);
9283              if (count($parts) != 2) {
9284                  continue;
9285              }
9286  
9287              if (strpos($subnet, ':') !== false) {
9288                  // IPv6.
9289                  if (!$ipv6) {
9290                      continue;
9291                  }
9292                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9293                  if ($ipstart === null) {
9294                      continue;
9295                  }
9296                  $ipparts = explode(':', $ipstart);
9297                  $start = hexdec(array_pop($ipparts));
9298                  $ipparts[] = trim($parts[1]);
9299                  $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9300                  if ($ipend === null) {
9301                      continue;
9302                  }
9303                  $ipparts[7] = '';
9304                  $ipnet = implode(':', $ipparts);
9305                  if (strpos($addr, $ipnet) !== 0) {
9306                      continue;
9307                  }
9308                  $ipparts = explode(':', $ipend);
9309                  $end = hexdec($ipparts[7]);
9310  
9311                  $addrend = hexdec($addrparts[7]);
9312  
9313                  if (($addrend >= $start) and ($addrend <= $end)) {
9314                      return true;
9315                  }
9316  
9317              } else {
9318                  // IPv4.
9319                  if ($ipv6) {
9320                      continue;
9321                  }
9322                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9323                  if ($ipstart === null) {
9324                      continue;
9325                  }
9326                  $ipparts = explode('.', $ipstart);
9327                  $ipparts[3] = trim($parts[1]);
9328                  $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9329                  if ($ipend === null) {
9330                      continue;
9331                  }
9332  
9333                  if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9334                      return true;
9335                  }
9336              }
9337  
9338          } else {
9339              // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9340              if (strpos($subnet, ':') !== false) {
9341                  // IPv6.
9342                  if (!$ipv6) {
9343                      continue;
9344                  }
9345                  $parts = explode(':', $subnet);
9346                  $count = count($parts);
9347                  if ($parts[$count-1] === '') {
9348                      unset($parts[$count-1]); // Trim trailing :'s.
9349                      $count--;
9350                      $subnet = implode('.', $parts);
9351                  }
9352                  $isip = cleanremoteaddr($subnet, false); // Normalise.
9353                  if ($isip !== null) {
9354                      if ($isip === $addr) {
9355                          return true;
9356                      }
9357                      continue;
9358                  } else if ($count > 8) {
9359                      continue;
9360                  }
9361                  $zeros = array_fill(0, 8-$count, '0');
9362                  $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9363                  if (address_in_subnet($addr, $subnet)) {
9364                      return true;
9365                  }
9366  
9367              } else {
9368                  // IPv4.
9369                  if ($ipv6) {
9370                      continue;
9371                  }
9372                  $parts = explode('.', $subnet);
9373                  $count = count($parts);
9374                  if ($parts[$count-1] === '') {
9375                      unset($parts[$count-1]); // Trim trailing .
9376                      $count--;
9377                      $subnet = implode('.', $parts);
9378                  }
9379                  if ($count == 4) {
9380                      $subnet = cleanremoteaddr($subnet, false); // Normalise.
9381                      if ($subnet === $addr) {
9382                          return true;
9383                      }
9384                      continue;
9385                  } else if ($count > 4) {
9386                      continue;
9387                  }
9388                  $zeros = array_fill(0, 4-$count, '0');
9389                  $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9390                  if (address_in_subnet($addr, $subnet)) {
9391                      return true;
9392                  }
9393              }
9394          }
9395      }
9396  
9397      return false;
9398  }
9399  
9400  /**
9401   * For outputting debugging info
9402   *
9403   * @param string $string The string to write
9404   * @param string $eol The end of line char(s) to use
9405   * @param string $sleep Period to make the application sleep
9406   *                      This ensures any messages have time to display before redirect
9407   */
9408  function mtrace($string, $eol="\n", $sleep=0) {
9409      global $CFG;
9410  
9411      if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9412          $fn = $CFG->mtrace_wrapper;
9413          $fn($string, $eol);
9414          return;
9415      } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9416          // We must explicitly call the add_line function here.
9417          // Uses of fwrite to STDOUT are not picked up by ob_start.
9418          if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9419              fwrite(STDOUT, $output);
9420          }
9421      } else {
9422          echo $string . $eol;
9423      }
9424  
9425      // Flush again.
9426      flush();
9427  
9428      // Delay to keep message on user's screen in case of subsequent redirect.
9429      if ($sleep) {
9430          sleep($sleep);
9431      }
9432  }
9433  
9434  /**
9435   * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9436   *
9437   * @param Throwable $e the error to ouptput.
9438   */
9439  function mtrace_exception(Throwable $e): void {
9440      $info = get_exception_info($e);
9441  
9442      $message = $info->message;
9443      if ($info->debuginfo) {
9444          $message .= "\n\n" . $info->debuginfo;
9445      }
9446      if ($info->backtrace) {
9447          $message .= "\n\n" . format_backtrace($info->backtrace, true);
9448      }
9449  
9450      mtrace($message);
9451  }
9452  
9453  /**
9454   * Replace 1 or more slashes or backslashes to 1 slash
9455   *
9456   * @param string $path The path to strip
9457   * @return string the path with double slashes removed
9458   */
9459  function cleardoubleslashes ($path) {
9460      return preg_replace('/(\/|\\\){1,}/', '/', $path);
9461  }
9462  
9463  /**
9464   * Is the current ip in a given list?
9465   *
9466   * @param string $list
9467   * @return bool
9468   */
9469  function remoteip_in_list($list) {
9470      $clientip = getremoteaddr(null);
9471  
9472      if (!$clientip) {
9473          // Ensure access on cli.
9474          return true;
9475      }
9476      return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9477  }
9478  
9479  /**
9480   * Returns most reliable client address
9481   *
9482   * @param string $default If an address can't be determined, then return this
9483   * @return string The remote IP address
9484   */
9485  function getremoteaddr($default='0.0.0.0') {
9486      global $CFG;
9487  
9488      if (!isset($CFG->getremoteaddrconf)) {
9489          // This will happen, for example, before just after the upgrade, as the
9490          // user is redirected to the admin screen.
9491          $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9492      } else {
9493          $variablestoskip = $CFG->getremoteaddrconf;
9494      }
9495      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9496          if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9497              $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9498              return $address ? $address : $default;
9499          }
9500      }
9501      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9502          if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9503              $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9504  
9505              $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9506                  global $CFG;
9507                  return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9508              });
9509  
9510              // Multiple proxies can append values to this header including an
9511              // untrusted original request header so we must only trust the last ip.
9512              $address = end($forwardedaddresses);
9513  
9514              if (substr_count($address, ":") > 1) {
9515                  // Remove port and brackets from IPv6.
9516                  if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9517                      $address = $matches[1];
9518                  }
9519              } else {
9520                  // Remove port from IPv4.
9521                  if (substr_count($address, ":") == 1) {
9522                      $parts = explode(":", $address);
9523                      $address = $parts[0];
9524                  }
9525              }
9526  
9527              $address = cleanremoteaddr($address);
9528              return $address ? $address : $default;
9529          }
9530      }
9531      if (!empty($_SERVER['REMOTE_ADDR'])) {
9532          $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9533          return $address ? $address : $default;
9534      } else {
9535          return $default;
9536      }
9537  }
9538  
9539  /**
9540   * Cleans an ip address. Internal addresses are now allowed.
9541   * (Originally local addresses were not allowed.)
9542   *
9543   * @param string $addr IPv4 or IPv6 address
9544   * @param bool $compress use IPv6 address compression
9545   * @return string normalised ip address string, null if error
9546   */
9547  function cleanremoteaddr($addr, $compress=false) {
9548      $addr = trim($addr);
9549  
9550      if (strpos($addr, ':') !== false) {
9551          // Can be only IPv6.
9552          $parts = explode(':', $addr);
9553          $count = count($parts);
9554  
9555          if (strpos($parts[$count-1], '.') !== false) {
9556              // Legacy ipv4 notation.
9557              $last = array_pop($parts);
9558              $ipv4 = cleanremoteaddr($last, true);
9559              if ($ipv4 === null) {
9560                  return null;
9561              }
9562              $bits = explode('.', $ipv4);
9563              $parts[] = dechex($bits[0]).dechex($bits[1]);
9564              $parts[] = dechex($bits[2]).dechex($bits[3]);
9565              $count = count($parts);
9566              $addr = implode(':', $parts);
9567          }
9568  
9569          if ($count < 3 or $count > 8) {
9570              return null; // Severly malformed.
9571          }
9572  
9573          if ($count != 8) {
9574              if (strpos($addr, '::') === false) {
9575                  return null; // Malformed.
9576              }
9577              // Uncompress.
9578              $insertat = array_search('', $parts, true);
9579              $missing = array_fill(0, 1 + 8 - $count, '0');
9580              array_splice($parts, $insertat, 1, $missing);
9581              foreach ($parts as $key => $part) {
9582                  if ($part === '') {
9583                      $parts[$key] = '0';
9584                  }
9585              }
9586          }
9587  
9588          $adr = implode(':', $parts);
9589          if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9590              return null; // Incorrect format - sorry.
9591          }
9592  
9593          // Normalise 0s and case.
9594          $parts = array_map('hexdec', $parts);
9595          $parts = array_map('dechex', $parts);
9596  
9597          $result = implode(':', $parts);
9598  
9599          if (!$compress) {
9600              return $result;
9601          }
9602  
9603          if ($result === '0:0:0:0:0:0:0:0') {
9604              return '::'; // All addresses.
9605          }
9606  
9607          $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9608          if ($compressed !== $result) {
9609              return $compressed;
9610          }
9611  
9612          $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9613          if ($compressed !== $result) {
9614              return $compressed;
9615          }
9616  
9617          $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9618          if ($compressed !== $result) {
9619              return $compressed;
9620          }
9621  
9622          return $result;
9623      }
9624  
9625      // First get all things that look like IPv4 addresses.
9626      $parts = array();
9627      if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9628          return null;
9629      }
9630      unset($parts[0]);
9631  
9632      foreach ($parts as $key => $match) {
9633          if ($match > 255) {
9634              return null;
9635          }
9636          $parts[$key] = (int)$match; // Normalise 0s.
9637      }
9638  
9639      return implode('.', $parts);
9640  }
9641  
9642  
9643  /**
9644   * Is IP address a public address?
9645   *
9646   * @param string $ip The ip to check
9647   * @return bool true if the ip is public
9648   */
9649  function ip_is_public($ip) {
9650      return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9651  }
9652  
9653  /**
9654   * This function will make a complete copy of anything it's given,
9655   * regardless of whether it's an object or not.
9656   *
9657   * @param mixed $thing Something you want cloned
9658   * @return mixed What ever it is you passed it
9659   */
9660  function fullclone($thing) {
9661      return unserialize(serialize($thing));
9662  }
9663  
9664  /**
9665   * Used to make sure that $min <= $value <= $max
9666   *
9667   * Make sure that value is between min, and max
9668   *
9669   * @param int $min The minimum value
9670   * @param int $value The value to check
9671   * @param int $max The maximum value
9672   * @return int
9673   */
9674  function bounded_number($min, $value, $max) {
9675      if ($value < $min) {
9676          return $min;
9677      }
9678      if ($value > $max) {
9679          return $max;
9680      }
9681      return $value;
9682  }
9683  
9684  /**
9685   * Check if there is a nested array within the passed array
9686   *
9687   * @param array $array
9688   * @return bool true if there is a nested array false otherwise
9689   */
9690  function array_is_nested($array) {
9691      foreach ($array as $value) {
9692          if (is_array($value)) {
9693              return true;
9694          }
9695      }
9696      return false;
9697  }
9698  
9699  /**
9700   * get_performance_info() pairs up with init_performance_info()
9701   * loaded in setup.php. Returns an array with 'html' and 'txt'
9702   * values ready for use, and each of the individual stats provided
9703   * separately as well.
9704   *
9705   * @return array
9706   */
9707  function get_performance_info() {
9708      global $CFG, $PERF, $DB, $PAGE;
9709  
9710      $info = array();
9711      $info['txt']  = me() . ' '; // Holds log-friendly representation.
9712  
9713      $info['html'] = '';
9714      if (!empty($CFG->themedesignermode)) {
9715          // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9716          $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9717      }
9718      $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9719  
9720      $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9721  
9722      $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9723      $info['txt'] .= 'time: '.$info['realtime'].'s ';
9724  
9725      // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9726      $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9727  
9728      if (function_exists('memory_get_usage')) {
9729          $info['memory_total'] = memory_get_usage();
9730          $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9731          $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9732          $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9733              $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9734      }
9735  
9736      if (function_exists('memory_get_peak_usage')) {
9737          $info['memory_peak'] = memory_get_peak_usage();
9738          $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9739          $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9740      }
9741  
9742      $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9743      $inc = get_included_files();
9744      $info['includecount'] = count($inc);
9745      $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9746      $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9747  
9748      if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9749          // We can not track more performance before installation or before PAGE init, sorry.
9750          return $info;
9751      }
9752  
9753      $filtermanager = filter_manager::instance();
9754      if (method_exists($filtermanager, 'get_performance_summary')) {
9755          list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9756          $info = array_merge($filterinfo, $info);
9757          foreach ($filterinfo as $key => $value) {
9758              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9759              $info['txt'] .= "$key: $value ";
9760          }
9761      }
9762  
9763      $stringmanager = get_string_manager();
9764      if (method_exists($stringmanager, 'get_performance_summary')) {
9765          list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9766          $info = array_merge($filterinfo, $info);
9767          foreach ($filterinfo as $key => $value) {
9768              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9769              $info['txt'] .= "$key: $value ";
9770          }
9771      }
9772  
9773      $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9774      $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9775      $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9776  
9777      if ($DB->want_read_slave()) {
9778          $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9779          $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9780          $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9781      }
9782  
9783      $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9784      $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9785      $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9786  
9787      if (function_exists('posix_times')) {
9788          $ptimes = posix_times();
9789          if (is_array($ptimes)) {
9790              foreach ($ptimes as $key => $val) {
9791                  $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9792              }
9793              $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9794              $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9795              $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9796          }
9797      }
9798  
9799      // Grab the load average for the last minute.
9800      // /proc will only work under some linux configurations
9801      // while uptime is there under MacOSX/Darwin and other unices.
9802      if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9803          list($serverload) = explode(' ', $loadavg[0]);
9804          unset($loadavg);
9805      } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9806          if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9807              $serverload = $matches[1];
9808          } else {
9809              trigger_error('Could not parse uptime output!');
9810          }
9811      }
9812      if (!empty($serverload)) {
9813          $info['serverload'] = $serverload;
9814          $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9815          $info['txt'] .= "serverload: {$info['serverload']} ";
9816      }
9817  
9818      // Display size of session if session started.
9819      if ($si = \core\session\manager::get_performance_info()) {
9820          $info['sessionsize'] = $si['size'];
9821          $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9822          $info['txt'] .= $si['txt'];
9823      }
9824  
9825      // Display time waiting for session if applicable.
9826      if (!empty($PERF->sessionlock['wait'])) {
9827          $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9828          $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9829              'class' => 'sessionwait col-sm-4'
9830          ]);
9831          $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9832      }
9833  
9834      $info['html'] .= '</ul>';
9835      $html = '';
9836      if ($stats = cache_helper::get_stats()) {
9837  
9838          $table = new html_table();
9839          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9840          $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9841          $table->data = [];
9842          $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9843  
9844          $text = 'Caches used (hits/misses/sets): ';
9845          $hits = 0;
9846          $misses = 0;
9847          $sets = 0;
9848          $maxstores = 0;
9849  
9850          // We want to align static caches into their own column.
9851          $hasstatic = false;
9852          foreach ($stats as $definition => $details) {
9853              $numstores = count($details['stores']);
9854              $first = key($details['stores']);
9855              if ($first !== cache_store::STATIC_ACCEL) {
9856                  $numstores++; // Add a blank space for the missing static store.
9857              }
9858              $maxstores = max($maxstores, $numstores);
9859          }
9860  
9861          $storec = 0;
9862  
9863          while ($storec++ < ($maxstores - 2)) {
9864              if ($storec == ($maxstores - 2)) {
9865                  $table->head[] = get_string('mappingfinal', 'cache');
9866              } else {
9867                  $table->head[] = "Store $storec";
9868              }
9869              $table->align[] = 'left';
9870              $table->align[] = 'right';
9871              $table->align[] = 'right';
9872              $table->align[] = 'right';
9873              $table->align[] = 'right';
9874              $table->head[] = 'H';
9875              $table->head[] = 'M';
9876              $table->head[] = 'S';
9877              $table->head[] = 'I/O';
9878          }
9879  
9880          ksort($stats);
9881  
9882          foreach ($stats as $definition => $details) {
9883              switch ($details['mode']) {
9884                  case cache_store::MODE_APPLICATION:
9885                      $modeclass = 'application';
9886                      $mode = ' <span title="application cache">App</span>';
9887                      break;
9888                  case cache_store::MODE_SESSION:
9889                      $modeclass = 'session';
9890                      $mode = ' <span title="session cache">Ses</span>';
9891                      break;
9892                  case cache_store::MODE_REQUEST:
9893                      $modeclass = 'request';
9894                      $mode = ' <span title="request cache">Req</span>';
9895                      break;
9896              }
9897              $row = [$mode, $definition];
9898  
9899              $text .= "$definition {";
9900  
9901              $storec = 0;
9902              foreach ($details['stores'] as $store => $data) {
9903  
9904                  if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9905                      $row[] = '';
9906                      $row[] = '';
9907                      $row[] = '';
9908                      $storec++;
9909                  }
9910  
9911                  $hits   += $data['hits'];
9912                  $misses += $data['misses'];
9913                  $sets   += $data['sets'];
9914                  if ($data['hits'] == 0 and $data['misses'] > 0) {
9915                      $cachestoreclass = 'nohits bg-danger';
9916                  } else if ($data['hits'] < $data['misses']) {
9917                      $cachestoreclass = 'lowhits bg-warning text-dark';
9918                  } else {
9919                      $cachestoreclass = 'hihits';
9920                  }
9921                  $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9922                  $cell = new html_table_cell($store);
9923                  $cell->attributes = ['class' => $cachestoreclass];
9924                  $row[] = $cell;
9925                  $cell = new html_table_cell($data['hits']);
9926                  $cell->attributes = ['class' => $cachestoreclass];
9927                  $row[] = $cell;
9928                  $cell = new html_table_cell($data['misses']);
9929                  $cell->attributes = ['class' => $cachestoreclass];
9930                  $row[] = $cell;
9931  
9932                  if ($store !== cache_store::STATIC_ACCEL) {
9933                      // The static cache is never set.
9934                      $cell = new html_table_cell($data['sets']);
9935                      $cell->attributes = ['class' => $cachestoreclass];
9936                      $row[] = $cell;
9937  
9938                      if ($data['hits'] || $data['sets']) {
9939                          if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9940                              $size = '-';
9941                          } else {
9942                              $size = display_size($data['iobytes'], 1, 'KB');
9943                              if ($data['iobytes'] >= 10 * 1024) {
9944                                  $cachestoreclass = ' bg-warning text-dark';
9945                              }
9946                          }
9947                      } else {
9948                          $size = '';
9949                      }
9950                      $cell = new html_table_cell($size);
9951                      $cell->attributes = ['class' => $cachestoreclass];
9952                      $row[] = $cell;
9953                  }
9954                  $storec++;
9955              }
9956              while ($storec++ < $maxstores) {
9957                  $row[] = '';
9958                  $row[] = '';
9959                  $row[] = '';
9960                  $row[] = '';
9961                  $row[] = '';
9962              }
9963              $text .= '} ';
9964  
9965              $table->data[] = $row;
9966          }
9967  
9968          $html .= html_writer::table($table);
9969  
9970          // Now lets also show sub totals for each cache store.
9971          $storetotals = [];
9972          $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9973          foreach ($stats as $definition => $details) {
9974              foreach ($details['stores'] as $store => $data) {
9975                  if (!array_key_exists($store, $storetotals)) {
9976                      $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9977                  }
9978                  $storetotals[$store]['class']   = $data['class'];
9979                  $storetotals[$store]['hits']   += $data['hits'];
9980                  $storetotals[$store]['misses'] += $data['misses'];
9981                  $storetotals[$store]['sets']   += $data['sets'];
9982                  $storetotal['hits']   += $data['hits'];
9983                  $storetotal['misses'] += $data['misses'];
9984                  $storetotal['sets']   += $data['sets'];
9985                  if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9986                      $storetotals[$store]['iobytes'] += $data['iobytes'];
9987                      $storetotal['iobytes'] += $data['iobytes'];
9988                  }
9989              }
9990          }
9991  
9992          $table = new html_table();
9993          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9994          $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9995          $table->data = [];
9996          $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9997  
9998          ksort($storetotals);
9999  
10000          foreach ($storetotals as $store => $data) {
10001              $row = [];
10002              if ($data['hits'] == 0 and $data['misses'] > 0) {
10003                  $cachestoreclass = 'nohits bg-danger';
10004              } else if ($data['hits'] < $data['misses']) {
10005                  $cachestoreclass = 'lowhits bg-warning text-dark';
10006              } else {
10007                  $cachestoreclass = 'hihits';
10008              }
10009              $cell = new html_table_cell($store);
10010              $cell->attributes = ['class' => $cachestoreclass];
10011              $row[] = $cell;
10012              $cell = new html_table_cell($data['class']);
10013              $cell->attributes = ['class' => $cachestoreclass];
10014              $row[] = $cell;
10015              $cell = new html_table_cell($data['hits']);
10016              $cell->attributes = ['class' => $cachestoreclass];
10017              $row[] = $cell;
10018              $cell = new html_table_cell($data['misses']);
10019              $cell->attributes = ['class' => $cachestoreclass];
10020              $row[] = $cell;
10021              $cell = new html_table_cell($data['sets']);
10022              $cell->attributes = ['class' => $cachestoreclass];
10023              $row[] = $cell;
10024              if ($data['hits'] || $data['sets']) {
10025                  if ($data['iobytes']) {
10026                      $size = display_size($data['iobytes'], 1, 'KB');
10027                  } else {
10028                      $size = '-';
10029                  }
10030              } else {
10031                  $size = '';
10032              }
10033              $cell = new html_table_cell($size);
10034              $cell->attributes = ['class' => $cachestoreclass];
10035              $row[] = $cell;
10036              $table->data[] = $row;
10037          }
10038          if (!empty($storetotal['iobytes'])) {
10039              $size = display_size($storetotal['iobytes'], 1, 'KB');
10040          } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
10041              $size = '-';
10042          } else {
10043              $size = '';
10044          }
10045          $row = [
10046              get_string('total'),
10047              '',
10048              $storetotal['hits'],
10049              $storetotal['misses'],
10050              $storetotal['sets'],
10051              $size,
10052          ];
10053          $table->data[] = $row;
10054  
10055          $html .= html_writer::table($table);
10056  
10057          $info['cachesused'] = "$hits / $misses / $sets";
10058          $info['html'] .= $html;
10059          $info['txt'] .= $text.'. ';
10060      } else {
10061          $info['cachesused'] = '0 / 0 / 0';
10062          $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
10063          $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
10064      }
10065  
10066      // Display lock information if any.
10067      if (!empty($PERF->locks)) {
10068          $table = new html_table();
10069          $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
10070          $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
10071          $table->align = ['left', 'right', 'center', 'right'];
10072          $table->data = [];
10073          $text = 'Locks (waited/obtained/held):';
10074          foreach ($PERF->locks as $locktiming) {
10075              $row = [];
10076              $row[] = s($locktiming->type . '/' . $locktiming->resource);
10077              $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
10078  
10079              // The time we had to wait to get the lock.
10080              $roundedtime = number_format($locktiming->wait, 1);
10081              $cell = new html_table_cell($roundedtime);
10082              if ($locktiming->wait > 0.5) {
10083                  $cell->attributes = ['class' => 'bg-warning text-dark'];
10084              }
10085              $row[] = $cell;
10086              $text .= $roundedtime . '/';
10087  
10088              // Show a tick or cross for success.
10089              $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
10090              $text .= ($locktiming->success ? 'y' : 'n') . '/';
10091  
10092              // If applicable, show how long we held the lock before releasing it.
10093              if (property_exists($locktiming, 'held')) {
10094                  $roundedtime = number_format($locktiming->held, 1);
10095                  $cell = new html_table_cell($roundedtime);
10096                  if ($locktiming->held > 0.5) {
10097                      $cell->attributes = ['class' => 'bg-warning text-dark'];
10098                  }
10099                  $row[] = $cell;
10100                  $text .= $roundedtime;
10101              } else {
10102                  $row[] = '-';
10103                  $text .= '-';
10104              }
10105              $text .= ')';
10106  
10107              $table->data[] = $row;
10108          }
10109          $info['html'] .= html_writer::table($table);
10110          $info['txt'] .= $text . '. ';
10111      }
10112  
10113      $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
10114      return $info;
10115  }
10116  
10117  /**
10118   * Renames a file or directory to a unique name within the same directory.
10119   *
10120   * This function is designed to avoid any potential race conditions, and select an unused name.
10121   *
10122   * @param string $filepath Original filepath
10123   * @param string $prefix Prefix to use for the temporary name
10124   * @return string|bool New file path or false if failed
10125   * @since Moodle 3.10
10126   */
10127  function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
10128      $dir = dirname($filepath);
10129      $basename = $dir . '/' . $prefix;
10130      $limit = 0;
10131      while ($limit < 100) {
10132          // Select a new name based on a random number.
10133          $newfilepath = $basename . md5(mt_rand());
10134  
10135          // Attempt a rename to that new name.
10136          if (@rename($filepath, $newfilepath)) {
10137              return $newfilepath;
10138          }
10139  
10140          // The first time, do some sanity checks, maybe it is failing for a good reason and there
10141          // is no point trying 100 times if so.
10142          if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
10143              return false;
10144          }
10145          $limit++;
10146      }
10147      return false;
10148  }
10149  
10150  /**
10151   * Delete directory or only its content
10152   *
10153   * @param string $dir directory path
10154   * @param bool $contentonly
10155   * @return bool success, true also if dir does not exist
10156   */
10157  function remove_dir($dir, $contentonly=false) {
10158      if (!is_dir($dir)) {
10159          // Nothing to do.
10160          return true;
10161      }
10162  
10163      if (!$contentonly) {
10164          // Start by renaming the directory; this will guarantee that other processes don't write to it
10165          // while it is in the process of being deleted.
10166          $tempdir = rename_to_unused_name($dir);
10167          if ($tempdir) {
10168              // If the rename was successful then delete the $tempdir instead.
10169              $dir = $tempdir;
10170          }
10171          // If the rename fails, we will continue through and attempt to delete the directory
10172          // without renaming it since that is likely to at least delete most of the files.
10173      }
10174  
10175      if (!$handle = opendir($dir)) {
10176          return false;
10177      }
10178      $result = true;
10179      while (false!==($item = readdir($handle))) {
10180          if ($item != '.' && $item != '..') {
10181              if (is_dir($dir.'/'.$item)) {
10182                  $result = remove_dir($dir.'/'.$item) && $result;
10183              } else {
10184                  $result = unlink($dir.'/'.$item) && $result;
10185              }
10186          }
10187      }
10188      closedir($handle);
10189      if ($contentonly) {
10190          clearstatcache(); // Make sure file stat cache is properly invalidated.
10191          return $result;
10192      }
10193      $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
10194      clearstatcache(); // Make sure file stat cache is properly invalidated.
10195      return $result;
10196  }
10197  
10198  /**
10199   * Detect if an object or a class contains a given property
10200   * will take an actual object or the name of a class
10201   *
10202   * @param mixed $obj Name of class or real object to test
10203   * @param string $property name of property to find
10204   * @return bool true if property exists
10205   */
10206  function object_property_exists( $obj, $property ) {
10207      if (is_string( $obj )) {
10208          $properties = get_class_vars( $obj );
10209      } else {
10210          $properties = get_object_vars( $obj );
10211      }
10212      return array_key_exists( $property, $properties );
10213  }
10214  
10215  /**
10216   * Converts an object into an associative array
10217   *
10218   * This function converts an object into an associative array by iterating
10219   * over its public properties. Because this function uses the foreach
10220   * construct, Iterators are respected. It works recursively on arrays of objects.
10221   * Arrays and simple values are returned as is.
10222   *
10223   * If class has magic properties, it can implement IteratorAggregate
10224   * and return all available properties in getIterator()
10225   *
10226   * @param mixed $var
10227   * @return array
10228   */
10229  function convert_to_array($var) {
10230      $result = array();
10231  
10232      // Loop over elements/properties.
10233      foreach ($var as $key => $value) {
10234          // Recursively convert objects.
10235          if (is_object($value) || is_array($value)) {
10236              $result[$key] = convert_to_array($value);
10237          } else {
10238              // Simple values are untouched.
10239              $result[$key] = $value;
10240          }
10241      }
10242      return $result;
10243  }
10244  
10245  /**
10246   * Detect a custom script replacement in the data directory that will
10247   * replace an existing moodle script
10248   *
10249   * @return string|bool full path name if a custom script exists, false if no custom script exists
10250   */
10251  function custom_script_path() {
10252      global $CFG, $SCRIPT;
10253  
10254      if ($SCRIPT === null) {
10255          // Probably some weird external script.
10256          return false;
10257      }
10258  
10259      $scriptpath = $CFG->customscripts . $SCRIPT;
10260  
10261      // Check the custom script exists.
10262      if (file_exists($scriptpath) and is_file($scriptpath)) {
10263          return $scriptpath;
10264      } else {
10265          return false;
10266      }
10267  }
10268  
10269  /**
10270   * Returns whether or not the user object is a remote MNET user. This function
10271   * is in moodlelib because it does not rely on loading any of the MNET code.
10272   *
10273   * @param object $user A valid user object
10274   * @return bool        True if the user is from a remote Moodle.
10275   */
10276  function is_mnet_remote_user($user) {
10277      global $CFG;
10278  
10279      if (!isset($CFG->mnet_localhost_id)) {
10280          include_once($CFG->dirroot . '/mnet/lib.php');
10281          $env = new mnet_environment();
10282          $env->init();
10283          unset($env);
10284      }
10285  
10286      return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10287  }
10288  
10289  /**
10290   * This function will search for browser prefereed languages, setting Moodle
10291   * to use the best one available if $SESSION->lang is undefined
10292   */
10293  function setup_lang_from_browser() {
10294      global $CFG, $SESSION, $USER;
10295  
10296      if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10297          // Lang is defined in session or user profile, nothing to do.
10298          return;
10299      }
10300  
10301      if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10302          return;
10303      }
10304  
10305      // Extract and clean langs from headers.
10306      $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10307      $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10308      $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10309      $langs = array();
10310  
10311      $order = 1.0;
10312      foreach ($rawlangs as $lang) {
10313          if (strpos($lang, ';') === false) {
10314              $langs[(string)$order] = $lang;
10315              $order = $order-0.01;
10316          } else {
10317              $parts = explode(';', $lang);
10318              $pos = strpos($parts[1], '=');
10319              $langs[substr($parts[1], $pos+1)] = $parts[0];
10320          }
10321      }
10322      krsort($langs, SORT_NUMERIC);
10323  
10324      // Look for such langs under standard locations.
10325      foreach ($langs as $lang) {
10326          // Clean it properly for include.
10327          $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10328          if (get_string_manager()->translation_exists($lang, false)) {
10329              // If the translation for this language exists then try to set it
10330              // for the rest of the session, if this is a read only session then
10331              // we can only set it temporarily in $CFG.
10332              if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
10333                  $CFG->lang = $lang;
10334              } else {
10335                  $SESSION->lang = $lang;
10336              }
10337              // We have finished. Go out.
10338              break;
10339          }
10340      }
10341      return;
10342  }
10343  
10344  /**
10345   * Check if $url matches anything in proxybypass list
10346   *
10347   * Any errors just result in the proxy being used (least bad)
10348   *
10349   * @param string $url url to check
10350   * @return boolean true if we should bypass the proxy
10351   */
10352  function is_proxybypass( $url ) {
10353      global $CFG;
10354  
10355      // Sanity check.
10356      if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10357          return false;
10358      }
10359  
10360      // Get the host part out of the url.
10361      if (!$host = parse_url( $url, PHP_URL_HOST )) {
10362          return false;
10363      }
10364  
10365      // Get the possible bypass hosts into an array.
10366      $matches = explode( ',', $CFG->proxybypass );
10367  
10368      // Check for a exact match on the IP or in the domains.
10369      $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10370      $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10371  
10372      if ($isdomaininallowedlist || $isipinsubnetlist) {
10373          return true;
10374      }
10375  
10376      // Nothing matched.
10377      return false;
10378  }
10379  
10380  /**
10381   * Check if the passed navigation is of the new style
10382   *
10383   * @param mixed $navigation
10384   * @return bool true for yes false for no
10385   */
10386  function is_newnav($navigation) {
10387      if (is_array($navigation) && !empty($navigation['newnav'])) {
10388          return true;
10389      } else {
10390          return false;
10391      }
10392  }
10393  
10394  /**
10395   * Checks whether the given variable name is defined as a variable within the given object.
10396   *
10397   * This will NOT work with stdClass objects, which have no class variables.
10398   *
10399   * @param string $var The variable name
10400   * @param object $object The object to check
10401   * @return boolean
10402   */
10403  function in_object_vars($var, $object) {
10404      $classvars = get_class_vars(get_class($object));
10405      $classvars = array_keys($classvars);
10406      return in_array($var, $classvars);
10407  }
10408  
10409  /**
10410   * Returns an array without repeated objects.
10411   * This function is similar to array_unique, but for arrays that have objects as values
10412   *
10413   * @param array $array
10414   * @param bool $keepkeyassoc
10415   * @return array
10416   */
10417  function object_array_unique($array, $keepkeyassoc = true) {
10418      $duplicatekeys = array();
10419      $tmp         = array();
10420  
10421      foreach ($array as $key => $val) {
10422          // Convert objects to arrays, in_array() does not support objects.
10423          if (is_object($val)) {
10424              $val = (array)$val;
10425          }
10426  
10427          if (!in_array($val, $tmp)) {
10428              $tmp[] = $val;
10429          } else {
10430              $duplicatekeys[] = $key;
10431          }
10432      }
10433  
10434      foreach ($duplicatekeys as $key) {
10435          unset($array[$key]);
10436      }
10437  
10438      return $keepkeyassoc ? $array : array_values($array);
10439  }
10440  
10441  /**
10442   * Is a userid the primary administrator?
10443   *
10444   * @param int $userid int id of user to check
10445   * @return boolean
10446   */
10447  function is_primary_admin($userid) {
10448      $primaryadmin =  get_admin();
10449  
10450      if ($userid == $primaryadmin->id) {
10451          return true;
10452      } else {
10453          return false;
10454      }
10455  }
10456  
10457  /**
10458   * Returns the site identifier
10459   *
10460   * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10461   */
10462  function get_site_identifier() {
10463      global $CFG;
10464      // Check to see if it is missing. If so, initialise it.
10465      if (empty($CFG->siteidentifier)) {
10466          set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10467      }
10468      // Return it.
10469      return $CFG->siteidentifier;
10470  }
10471  
10472  /**
10473   * Check whether the given password has no more than the specified
10474   * number of consecutive identical characters.
10475   *
10476   * @param string $password   password to be checked against the password policy
10477   * @param integer $maxchars  maximum number of consecutive identical characters
10478   * @return bool
10479   */
10480  function check_consecutive_identical_characters($password, $maxchars) {
10481  
10482      if ($maxchars < 1) {
10483          return true; // Zero 0 is to disable this check.
10484      }
10485      if (strlen($password) <= $maxchars) {
10486          return true; // Too short to fail this test.
10487      }
10488  
10489      $previouschar = '';
10490      $consecutivecount = 1;
10491      foreach (str_split($password) as $char) {
10492          if ($char != $previouschar) {
10493              $consecutivecount = 1;
10494          } else {
10495              $consecutivecount++;
10496              if ($consecutivecount > $maxchars) {
10497                  return false; // Check failed already.
10498              }
10499          }
10500  
10501          $previouschar = $char;
10502      }
10503  
10504      return true;
10505  }
10506  
10507  /**
10508   * Helper function to do partial function binding.
10509   * so we can use it for preg_replace_callback, for example
10510   * this works with php functions, user functions, static methods and class methods
10511   * it returns you a callback that you can pass on like so:
10512   *
10513   * $callback = partial('somefunction', $arg1, $arg2);
10514   *     or
10515   * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10516   *     or even
10517   * $obj = new someclass();
10518   * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10519   *
10520   * and then the arguments that are passed through at calltime are appended to the argument list.
10521   *
10522   * @param mixed $function a php callback
10523   * @param mixed $arg1,... $argv arguments to partially bind with
10524   * @return array Array callback
10525   */
10526  function partial() {
10527      if (!class_exists('partial')) {
10528          /**
10529           * Used to manage function binding.
10530           * @copyright  2009 Penny Leach
10531           * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10532           */
10533          class partial{
10534              /** @var array */
10535              public $values = array();
10536              /** @var string The function to call as a callback. */
10537              public $func;
10538              /**
10539               * Constructor
10540               * @param string $func
10541               * @param array $args
10542               */
10543              public function __construct($func, $args) {
10544                  $this->values = $args;
10545                  $this->func = $func;
10546              }
10547              /**
10548               * Calls the callback function.
10549               * @return mixed
10550               */
10551              public function method() {
10552                  $args = func_get_args();
10553                  return call_user_func_array($this->func, array_merge($this->values, $args));
10554              }
10555          }
10556      }
10557      $args = func_get_args();
10558      $func = array_shift($args);
10559      $p = new partial($func, $args);
10560      return array($p, 'method');
10561  }
10562  
10563  /**
10564   * helper function to load up and initialise the mnet environment
10565   * this must be called before you use mnet functions.
10566   *
10567   * @return mnet_environment the equivalent of old $MNET global
10568   */
10569  function get_mnet_environment() {
10570      global $CFG;
10571      require_once($CFG->dirroot . '/mnet/lib.php');
10572      static $instance = null;
10573      if (empty($instance)) {
10574          $instance = new mnet_environment();
10575          $instance->init();
10576      }
10577      return $instance;
10578  }
10579  
10580  /**
10581   * during xmlrpc server code execution, any code wishing to access
10582   * information about the remote peer must use this to get it.
10583   *
10584   * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10585   */
10586  function get_mnet_remote_client() {
10587      if (!defined('MNET_SERVER')) {
10588          debugging(get_string('notinxmlrpcserver', 'mnet'));
10589          return false;
10590      }
10591      global $MNET_REMOTE_CLIENT;
10592      if (isset($MNET_REMOTE_CLIENT)) {
10593          return $MNET_REMOTE_CLIENT;
10594      }
10595      return false;
10596  }
10597  
10598  /**
10599   * during the xmlrpc server code execution, this will be called
10600   * to setup the object returned by {@link get_mnet_remote_client}
10601   *
10602   * @param mnet_remote_client $client the client to set up
10603   * @throws moodle_exception
10604   */
10605  function set_mnet_remote_client($client) {
10606      if (!defined('MNET_SERVER')) {
10607          throw new moodle_exception('notinxmlrpcserver', 'mnet');
10608      }
10609      global $MNET_REMOTE_CLIENT;
10610      $MNET_REMOTE_CLIENT = $client;
10611  }
10612  
10613  /**
10614   * return the jump url for a given remote user
10615   * this is used for rewriting forum post links in emails, etc
10616   *
10617   * @param stdclass $user the user to get the idp url for
10618   */
10619  function mnet_get_idp_jump_url($user) {
10620      global $CFG;
10621  
10622      static $mnetjumps = array();
10623      if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10624          $idp = mnet_get_peer_host($user->mnethostid);
10625          $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10626          $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10627      }
10628      return $mnetjumps[$user->mnethostid];
10629  }
10630  
10631  /**
10632   * Gets the homepage to use for the current user
10633   *
10634   * @return int One of HOMEPAGE_*
10635   */
10636  function get_home_page() {
10637      global $CFG;
10638  
10639      if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10640          // If dashboard is disabled, home will be set to default page.
10641          $defaultpage = get_default_home_page();
10642          if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10643              if (!empty($CFG->enabledashboard)) {
10644                  return HOMEPAGE_MY;
10645              } else {
10646                  return $defaultpage;
10647              }
10648          } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10649              return HOMEPAGE_MYCOURSES;
10650          } else {
10651              $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10652              if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10653                  // If the user was using the dashboard but it's disabled, return the default home page.
10654                  $userhomepage = $defaultpage;
10655              }
10656              return $userhomepage;
10657          }
10658      }
10659      return HOMEPAGE_SITE;
10660  }
10661  
10662  /**
10663   * Returns the default home page to display if current one is not defined or can't be applied.
10664   * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10665   *
10666   * @return int The default home page.
10667   */
10668  function get_default_home_page(): int {
10669      global $CFG;
10670  
10671      return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10672  }
10673  
10674  /**
10675   * Gets the name of a course to be displayed when showing a list of courses.
10676   * By default this is just $course->fullname but user can configure it. The
10677   * result of this function should be passed through print_string.
10678   * @param stdClass|core_course_list_element $course Moodle course object
10679   * @return string Display name of course (either fullname or short + fullname)
10680   */
10681  function get_course_display_name_for_list($course) {
10682      global $CFG;
10683      if (!empty($CFG->courselistshortnames)) {
10684          if (!($course instanceof stdClass)) {
10685              $course = (object)convert_to_array($course);
10686          }
10687          return get_string('courseextendednamedisplay', '', $course);
10688      } else {
10689          return $course->fullname;
10690      }
10691  }
10692  
10693  /**
10694   * Safe analogue of unserialize() that can only parse arrays
10695   *
10696   * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10697   *
10698   * @param string $expression
10699   * @return array|bool either parsed array or false if parsing was impossible.
10700   */
10701  function unserialize_array($expression) {
10702  
10703      // Check the expression is an array.
10704      if (!preg_match('/^a:(\d+):/', $expression)) {
10705          return false;
10706      }
10707  
10708      $values = (array) unserialize_object($expression);
10709  
10710      // Callback that returns true if the given value is an unserialized object, executes recursively.
10711      $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10712          if (is_array($value)) {
10713              return (bool) array_filter($value, $invalidvaluecallback);
10714          }
10715          return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10716      };
10717  
10718      // Iterate over the result to ensure there are no stray objects.
10719      if (array_filter($values, $invalidvaluecallback)) {
10720          return false;
10721      }
10722  
10723      return $values;
10724  }
10725  
10726  /**
10727   * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10728   *
10729   * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10730   * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10731   * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10732   *
10733   * @param string $input
10734   * @return stdClass
10735   */
10736  function unserialize_object(string $input): stdClass {
10737      $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10738      return (object) $instance;
10739  }
10740  
10741  /**
10742   * The lang_string class
10743   *
10744   * This special class is used to create an object representation of a string request.
10745   * It is special because processing doesn't occur until the object is first used.
10746   * The class was created especially to aid performance in areas where strings were
10747   * required to be generated but were not necessarily used.
10748   * As an example the admin tree when generated uses over 1500 strings, of which
10749   * normally only 1/3 are ever actually printed at any time.
10750   * The performance advantage is achieved by not actually processing strings that
10751   * arn't being used, as such reducing the processing required for the page.
10752   *
10753   * How to use the lang_string class?
10754   *     There are two methods of using the lang_string class, first through the
10755   *     forth argument of the get_string function, and secondly directly.
10756   *     The following are examples of both.
10757   * 1. Through get_string calls e.g.
10758   *     $string = get_string($identifier, $component, $a, true);
10759   *     $string = get_string('yes', 'moodle', null, true);
10760   * 2. Direct instantiation
10761   *     $string = new lang_string($identifier, $component, $a, $lang);
10762   *     $string = new lang_string('yes');
10763   *
10764   * How do I use a lang_string object?
10765   *     The lang_string object makes use of a magic __toString method so that you
10766   *     are able to use the object exactly as you would use a string in most cases.
10767   *     This means you are able to collect it into a variable and then directly
10768   *     echo it, or concatenate it into another string, or similar.
10769   *     The other thing you can do is manually get the string by calling the
10770   *     lang_strings out method e.g.
10771   *         $string = new lang_string('yes');
10772   *         $string->out();
10773   *     Also worth noting is that the out method can take one argument, $lang which
10774   *     allows the developer to change the language on the fly.
10775   *
10776   * When should I use a lang_string object?
10777   *     The lang_string object is designed to be used in any situation where a
10778   *     string may not be needed, but needs to be generated.
10779   *     The admin tree is a good example of where lang_string objects should be
10780   *     used.
10781   *     A more practical example would be any class that requries strings that may
10782   *     not be printed (after all classes get renderer by renderers and who knows
10783   *     what they will do ;))
10784   *
10785   * When should I not use a lang_string object?
10786   *     Don't use lang_strings when you are going to use a string immediately.
10787   *     There is no need as it will be processed immediately and there will be no
10788   *     advantage, and in fact perhaps a negative hit as a class has to be
10789   *     instantiated for a lang_string object, however get_string won't require
10790   *     that.
10791   *
10792   * Limitations:
10793   * 1. You cannot use a lang_string object as an array offset. Doing so will
10794   *     result in PHP throwing an error. (You can use it as an object property!)
10795   *
10796   * @package    core
10797   * @category   string
10798   * @copyright  2011 Sam Hemelryk
10799   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10800   */
10801  class lang_string {
10802  
10803      /** @var string The strings identifier */
10804      protected $identifier;
10805      /** @var string The strings component. Default '' */
10806      protected $component = '';
10807      /** @var array|stdClass Any arguments required for the string. Default null */
10808      protected $a = null;
10809      /** @var string The language to use when processing the string. Default null */
10810      protected $lang = null;
10811  
10812      /** @var string The processed string (once processed) */
10813      protected $string = null;
10814  
10815      /**
10816       * A special boolean. If set to true then the object has been woken up and
10817       * cannot be regenerated. If this is set then $this->string MUST be used.
10818       * @var bool
10819       */
10820      protected $forcedstring = false;
10821  
10822      /**
10823       * Constructs a lang_string object
10824       *
10825       * This function should do as little processing as possible to ensure the best
10826       * performance for strings that won't be used.
10827       *
10828       * @param string $identifier The strings identifier
10829       * @param string $component The strings component
10830       * @param stdClass|array|mixed $a Any arguments the string requires
10831       * @param string $lang The language to use when processing the string.
10832       * @throws coding_exception
10833       */
10834      public function __construct($identifier, $component = '', $a = null, $lang = null) {
10835          if (empty($component)) {
10836              $component = 'moodle';
10837          }
10838  
10839          $this->identifier = $identifier;
10840          $this->component = $component;
10841          $this->lang = $lang;
10842  
10843          // We MUST duplicate $a to ensure that it if it changes by reference those
10844          // changes are not carried across.
10845          // To do this we always ensure $a or its properties/values are strings
10846          // and that any properties/values that arn't convertable are forgotten.
10847          if ($a !== null) {
10848              if (is_scalar($a)) {
10849                  $this->a = $a;
10850              } else if ($a instanceof lang_string) {
10851                  $this->a = $a->out();
10852              } else if (is_object($a) or is_array($a)) {
10853                  $a = (array)$a;
10854                  $this->a = array();
10855                  foreach ($a as $key => $value) {
10856                      // Make sure conversion errors don't get displayed (results in '').
10857                      if (is_array($value)) {
10858                          $this->a[$key] = '';
10859                      } else if (is_object($value)) {
10860                          if (method_exists($value, '__toString')) {
10861                              $this->a[$key] = $value->__toString();
10862                          } else {
10863                              $this->a[$key] = '';
10864                          }
10865                      } else {
10866                          $this->a[$key] = (string)$value;
10867                      }
10868                  }
10869              }
10870          }
10871  
10872          if (debugging(false, DEBUG_DEVELOPER)) {
10873              if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10874                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10875              }
10876              if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10877                  throw new coding_exception('Invalid string compontent. Please check your string definition');
10878              }
10879              if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10880                  debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10881              }
10882          }
10883      }
10884  
10885      /**
10886       * Processes the string.
10887       *
10888       * This function actually processes the string, stores it in the string property
10889       * and then returns it.
10890       * You will notice that this function is VERY similar to the get_string method.
10891       * That is because it is pretty much doing the same thing.
10892       * However as this function is an upgrade it isn't as tolerant to backwards
10893       * compatibility.
10894       *
10895       * @return string
10896       * @throws coding_exception
10897       */
10898      protected function get_string() {
10899          global $CFG;
10900  
10901          // Check if we need to process the string.
10902          if ($this->string === null) {
10903              // Check the quality of the identifier.
10904              if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10905                  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);
10906              }
10907  
10908              // Process the string.
10909              $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10910              // Debugging feature lets you display string identifier and component.
10911              if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10912                  $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10913              }
10914          }
10915          // Return the string.
10916          return $this->string;
10917      }
10918  
10919      /**
10920       * Returns the string
10921       *
10922       * @param string $lang The langauge to use when processing the string
10923       * @return string
10924       */
10925      public function out($lang = null) {
10926          if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10927              if ($this->forcedstring) {
10928                  debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10929                  return $this->get_string();
10930              }
10931              $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10932              return $translatedstring->out();
10933          }
10934          return $this->get_string();
10935      }
10936  
10937      /**
10938       * Magic __toString method for printing a string
10939       *
10940       * @return string
10941       */
10942      public function __toString() {
10943          return $this->get_string();
10944      }
10945  
10946      /**
10947       * Magic __set_state method used for var_export
10948       *
10949       * @param array $array
10950       * @return self
10951       */
10952      public static function __set_state(array $array): self {
10953          $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10954          $tmp->string = $array['string'];
10955          $tmp->forcedstring = $array['forcedstring'];
10956          return $tmp;
10957      }
10958  
10959      /**
10960       * Prepares the lang_string for sleep and stores only the forcedstring and
10961       * string properties... the string cannot be regenerated so we need to ensure
10962       * it is generated for this.
10963       *
10964       * @return string
10965       */
10966      public function __sleep() {
10967          $this->get_string();
10968          $this->forcedstring = true;
10969          return array('forcedstring', 'string', 'lang');
10970      }
10971  
10972      /**
10973       * Returns the identifier.
10974       *
10975       * @return string
10976       */
10977      public function get_identifier() {
10978          return $this->identifier;
10979      }
10980  
10981      /**
10982       * Returns the component.
10983       *
10984       * @return string
10985       */
10986      public function get_component() {
10987          return $this->component;
10988      }
10989  }
10990  
10991  /**
10992   * Get human readable name describing the given callable.
10993   *
10994   * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10995   * It does not check if the callable actually exists.
10996   *
10997   * @param callable|string|array $callable
10998   * @return string|bool Human readable name of callable, or false if not a valid callable.
10999   */
11000  function get_callable_name($callable) {
11001  
11002      if (!is_callable($callable, true, $name)) {
11003          return false;
11004  
11005      } else {
11006          return $name;
11007      }
11008  }
11009  
11010  /**
11011   * Tries to guess if $CFG->wwwroot is publicly accessible or not.
11012   * Never put your faith on this function and rely on its accuracy as there might be false positives.
11013   * It just performs some simple checks, and mainly is used for places where we want to hide some options
11014   * such as site registration when $CFG->wwwroot is not publicly accessible.
11015   * Good thing is there is no false negative.
11016   * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
11017   *
11018   * @return bool
11019   */
11020  function site_is_public() {
11021      global $CFG;
11022  
11023      // Return early if site admin has forced this setting.
11024      if (isset($CFG->site_is_public)) {
11025          return (bool)$CFG->site_is_public;
11026      }
11027  
11028      $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
11029  
11030      if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
11031          $ispublic = false;
11032      } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
11033          $ispublic = false;
11034      } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
11035          $ispublic = false;
11036      } else {
11037          $ispublic = true;
11038      }
11039  
11040      return $ispublic;
11041  }
11042  
11043  /**
11044   * Validates user's password length.
11045   *
11046   * @param string $password
11047   * @param int $pepperlength The length of the used peppers
11048   * @return bool
11049   */
11050  function exceeds_password_length(string $password, int $pepperlength = 0): bool {
11051      return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
11052  }